mirror of
https://github.com/neovim/neovim.git
synced 2026-08-02 20:23:54 +08:00
Problem:
POSIX-compatible Ex-mode requires special-cases all over the codebase to
match various quirks that don't actually matter to users.
- The main utility of *interactive* Ex-mode is its REPL behavior, and
that can be achieved with `cmdwin`, which also gains extra UX
benefits.
- The main utility of *non-interactive* `nvim -es` is for shell
scripting, where Ex-mode quirks are mostly unhelpful (e.g. the
"Entering Ex mode" message).
Solution:
- Reimplement *interactive* Ex-mode as a "persistent, insert-mode
cmdwin" in Lua.
- "nvim -e/-E" is simply an alias to "gQ".
- Reframe *non-interactive* Ex-mode (`nvim -es`) as "script mode".
- Drop POSIX Ex-mode quirks.
Improvements:
- "nvim -V1 -es" output ends with a final newline!
- "nvim -V1 -es" no longer shows the "Entering Ex mode" msg. (This was
pointless noise, unwanted for scripting purposes.)
- stdin is no longer typeahead. Scripts (":lua io.read()") can read
stdin as data.
- Empty line is a no-op: a stray blank line no longer moves the cursor
(deviates from POSIX ex "+1"), no longer exits 1 at EOF (E501).
Preserved behavior:
- cursor starts at "$"
- mode()=="cv" (for non-interactive)
- multiline commands (:append/:function/heredoc pull continuation lines)
- bare-range print
- :print=>stdout
- -V1=>stderr
- CRLF input
- continue-after-error and exit codes
Dropped (regressed) POSIX behavior (non-interactive):
- Event loop only ticks while/between commands, not while blocked
waiting for a stdin line.
- ":g/pat/visual...Q"
- input()/getchar()/":s/x/y/c" no longer consume stdin lines as
answers: Nvim stops at end-of-input, skipping the rest of the script,
exit 0. Use ":lua io.read()" instead.
- If users care about this they should use interactive Ex-mode (`gQ`).
- ":@r" stops at end of the register instead of continuing to read
cmdline input from stdin.
222 lines
7.2 KiB
Lua
222 lines
7.2 KiB
Lua
-- 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<string, true>
|
|
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.<user>/"
|
|
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
|