-- For "--listen", ":restart", and related remote/server functionality. local util = require('vim._core.util') local M = {} --- Called by builtin serverlist(). Returns the combined server list (own + peers). --- ---@class vim.ServerInfo ---@field addr string Server address (socket path, named pipe, or TCP host:port). ---@field pid integer PID of the Nvim process. ---@field own boolean True if this server belongs to the current Nvim instance. ---@field active integer Timestamp of last user activity. --- @param opts? table Options: --- - opts.peer (boolean): If true, also discover peer servers. --- - opts.info (boolean): If true, return a list of |vim.ServerInfo| dicts --- instead of a list of addresses. Implies peer=true. --- @param addrs string[] Internal ("own") addresses, from server_address_list. --- @return string[]|vim.ServerInfo[] function M.serverlist(opts, addrs) if type(opts) ~= 'table' then return addrs end local want_info = opts.info == true if opts.peer ~= true and not want_info then return addrs end local seen = {} ---@type table for _, a in ipairs(addrs) do seen[a] = true end local peers ---@type string[]|vim.ServerInfo[] if want_info then local self_pid = vim.fn.getpid() local self_active = vim.v.useractive peers = vim.tbl_map(function(addr) ---@param addr vim.ServerInfo return { addr = addr, pid = self_pid, own = true, active = self_active, } end, addrs) else peers = addrs end -- Discover peer servers in stdpath("run"). -- TODO: track TCP servers, somehow. -- TODO: support Windows named pipes. local root = vim.fs.normalize(vim.fn.stdpath('run') .. '/..') local socket_paths = vim.fs.find(function(name, _) return name:match('nvim.*') end, { path = root, type = 'socket', limit = math.huge }) for _, socket in ipairs(socket_paths) do if not seen[socket] then local ok, chan = pcall(vim.fn.sockconnect, 'pipe', socket, { rpc = true }) if ok and chan and chan > 0 then -- Check that the server is responding -- TODO: do we need a timeout or error handling here? local ok_rpc, peer_info = pcall( vim.fn.rpcrequest, chan, 'nvim_exec_lua', [[return { pid = vim.fn.getpid(), active = vim.v.useractive, }]], {} ) if ok_rpc and type(peer_info) == 'table' then ---@cast peer_info vim.ServerInfo seen[socket] = true if want_info then table.insert(peers, { addr = socket, pid = peer_info.pid, own = false, active = peer_info.active, }) else table.insert(peers, socket) end end pcall(vim.fn.chanclose, chan) end end end return peers end -- (Windows only) Canonical --listen address persisted across restarts. M.restart_canonical_addr = nil ---@type string? --- (Windows only) --- Called on the new server via nvim_exec_lua RPC from the old server (:restart). --- Windows named pipes can't be rebound immediately, so the new server starts on a --- temporary bootstrap address and polls until the canonical address is reclaimable. --- @param canonical_addr string The original --listen address to reclaim. --- @param expected_uis integer Number of UIs expected to reattach (0 = don't wait). function M.rebind_after_restart(canonical_addr, expected_uis) M.restart_canonical_addr = canonical_addr local bootstrap_addr = vim.v.servername -- Temporary autogenerated address. local poll_ms = 50 local max_wait_ms = 30000 local timer = assert(vim.uv.new_timer()) -- Poll until the canonical address can be reclaimed (or timeout). local poll_elapsed = 0 timer:start(poll_ms, poll_ms, function() vim.schedule(function() if timer:is_closing() then return end poll_elapsed = poll_elapsed + poll_ms if poll_elapsed >= max_wait_ms then timer:stop() timer:close() return end if not vim.list_contains(vim.fn.serverlist(), canonical_addr) then local ok = vim._with({ log_level = 5 }, function() return pcall(vim.fn.serverstart, canonical_addr) end) if not ok then return -- pipe still held by old server; retry next tick end end -- Wait for UIs to reattach, then retire the bootstrap address. local elapsed = 0 timer:stop() timer:start(poll_ms, poll_ms, function() vim.schedule(function() if timer:is_closing() then return end elapsed = elapsed + poll_ms local all_uis = expected_uis <= 0 or #vim.api.nvim_list_uis() >= expected_uis if all_uis or elapsed >= max_wait_ms then if canonical_addr ~= bootstrap_addr then vim._with({ log_level = 5 }, function() pcall(vim.fn.serverstop, bootstrap_addr) end) end timer:stop() timer:close() end end) end) end) end) end -- Called by ex_restart(). Saves the current session and calls ex_restart() (again) with the -- updated arguments. -- -- TODO: https://github.com/neovim/neovim/issues/34204 -- --- @param eap vim._core.ExCmdArgs --- @param extra { quit_cmd: string } function M.ex_session_restart(eap, extra) local after_cmd = eap.args -- User-provided [command]. assert(not after_cmd:find(']==]')) -- Use custom +cmd if given. local quit_cmd = extra.quit_cmd == '' and 'qall' or extra.quit_cmd -- Preserve v:this_session in the restarted Nvim. local this_session = vim.v.this_session assert(not this_session:find(']==]')) -- Nvim temp "root dir": "/tmp/…/nvim./" local tmproot = vim.fs.abspath(vim.fs.dirname(vim.fs.dirname(vim.fn.tempname()))) assert(not tmproot:find(']==]')) local fd, session = vim.uv.fs_mkstemp(vim.fs.joinpath(tmproot, 'restart_session_XXXXXX')) if not fd then error('Failed to get temporary filename for restart session') end vim.uv.fs_close(fd) -- Write session local session_arg = vim.fn.fnameescape(session) vim.cmd('%argdelete') vim.cmd.mksession { session_arg, bang = true } -- Lua commands to restore the session and remove the session file local after_list = {} table.insert(after_list, ('vim.cmd("source %s")'):format(session_arg)) table.insert(after_list, ('pcall(vim.fs.rm, [==[%s]==])'):format(session)) table.insert(after_list, ('vim.v.this_session = [==[%s]==]'):format(this_session)) if after_cmd ~= '' then table.insert(after_list, ('vim.cmd([==[%s]==])'):format(after_cmd)) end local after = 'lua ' .. table.concat(after_list, ';') -- Restart Neovim and run our Lua commands local success, msg = pcall(function() -- "+:::" dummy prefix tells the C handler that this is actually a non-bang restart. -- Then v:startreason (if any) can be preserved. vim.cmd.restart { '+:::', quit_cmd, after, bang = true } end) if not success then --- @cast msg string vim.fs.rm(session, { force = true }) -- Trim error message to be equivalent to `:restart!` if msg:find('Vim:') then util.echo_err(util.cmd_errmsg(msg)) else error(msg) end end end return M