Szymon Wilczek

research / neovim/neovim / feat(server) / c3f803c3b6d4

return detailed info from serverlist({info=true})

neovim/neovim

feat(server) +125 −12

From: Szymon Wilczek <[email protected]>
Date: 25 Apr 2026
Subject: feat(server): return detailed info from serverlist({info=true})
Pull request: #39392
Merged: 6 May 2026, by Justin M. Keyes

Problem

UI tools and orchestration engines need more context than just raw socket addresses from serverlist(). Without knowing if a server belongs to the current instance or knowing its PID, UIs cannot display meaningful options to users.

Solution

  • Added the info=v:true option to serverlist().
  • When info is requested, it implies peer=true and returns a list of dictionaries (defined as vim.ServerInfo) with addr, pid and own.
  • Uses an RPC request to getpid() across the socket to fetch the peer's actual process ID.

This is prerequisite API enhancement suggested by @justinmk for

signed, reported, fixes

  • Author Szymon Wilczek author
  • Merged-by Justin M. Keyes maintainer · merged it

diff

runtime/doc/news.txt +3 −0
@@ -225,6 +225,9 @@ VIMSCRIPT  • |v:exitreason| is set before |QuitPre|. • |v:starttime| is the process start time (nanoseconds since UNIX epoch).+• |serverlist()| accepts a new `info` option that returns a list of dicts+  with `addr`, `pid` and `own` for each server (own + peers),+  enabling pickers like the future `:connect` host selector.  ============================================================================== CHANGED FEATURES                                                 *news-changed*
runtime/doc/vimfn.txt +13 −1
@@ -9139,15 +9139,27 @@ serverlist([{opts}])                                              *serverlist()* 		          will also be returned. (default: |FALSE|) 		          Not supported on Windows yet. +		  info  : If |TRUE|, return a list of Dicts with detailed info+		          instead of addresses. Implies `peer=true`. Each Dict+		          has the following items:+		            addr   (string)  Server address.+		            pid    (number?) PID of the Nvim process owning the+		                             server, or |v:null| if the peer is+		                             unreachable.+		            own    (bool)    Whether this server belongs to the+		                             current Nvim instance.+		          (default: |FALSE|)+ 		Example: >vim 			echo serverlist()+			echo serverlist(#{info: v:true}) <                  Parameters: ~                   • {opts} (`table?`)                  Return: ~-                  (`string[]`)+                  (`string[]|vim.ServerInfo[]`)  serverstart([{address}])                                         *serverstart()* 		Opens a socket or named pipe at {address} and listens for
runtime/lua/vim/_core/server.lua +52 −9
@@ -4,15 +4,35 @@ 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 owning this server (nil if unreachable).+---@field own boolean True if this server belongs to the current Nvim instance.+ --- @param opts? table Options:----              - opts.peer is true, also discover peer servers.---- @param addrs string[] Internal ("own") addresses, from `server_address_list`.---- @return string[] # Combined list of servers (own + peers).+---              - 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' or not opts.peer then+  if type(opts) ~= 'table' then     return addrs   end +  local peer = opts.peer == true+  local want_info = opts.info == true+  local discover = peer or want_info++  if not discover then+    return addrs+  end++  local own_set = {} ---@type table<string, true>+  for _, a in ipairs(addrs) do+    own_set[a] = true+  end+   -- Discover peer servers in stdpath("run").   -- TODO: track TCP servers, somehow.   -- TODO: support Windows named pipes.@@ -21,21 +41,44 @@ function M.serverlist(opts, addrs)     return name:match('nvim.*')   end, { path = root, type = 'socket', limit = math.huge }) +  local peer_pids = {} ---@type table<string, integer>+   for _, socket in ipairs(socket_paths) do-    if not vim.list_contains(addrs, socket) then+    if not own_set[socket] and not vim.list_contains(addrs, socket) then       local ok, chan = pcall(vim.fn.sockconnect, 'pipe', socket, { rpc = true })-      if ok and chan then+      if ok and chan and chan > 0 then         -- Check that the server is responding         -- TODO: do we need a timeout or error handling here?-        if vim.fn.rpcrequest(chan, 'nvim_get_chan_info', 0).id then+        local ok_chan, chan_info = pcall(vim.fn.rpcrequest, chan, 'nvim_get_chan_info', 0)+        if ok_chan and type(chan_info) == 'table' and chan_info.id then           table.insert(addrs, socket)+          if want_info then+            local ok_pid, pid = pcall(vim.fn.rpcrequest, chan, 'nvim_eval', 'getpid()')+            if ok_pid and type(pid) == 'number' then+              peer_pids[socket] = pid+            end+          end         end-        vim.fn.chanclose(chan)+        pcall(vim.fn.chanclose, chan)       end     end   end -  return addrs+  if not want_info then+    return addrs+  end++  local self_pid = vim.fn.getpid()+  local result = {} ---@type vim.ServerInfo[]+  for _, addr in ipairs(addrs) do+    local own = own_set[addr] == true+    table.insert(result, {+      addr = addr,+      pid = own and self_pid or peer_pids[addr],+      own = own,+    })+  end+  return result end  -- (Windows only) Canonical --listen address persisted across restarts.
runtime/lua/vim/_meta/vimfn.gen.lua +13 −1
@@ -8289,12 +8289,24 @@ function vim.fn.searchpos(pattern, flags, stopline, timeout, skip) end ---           will also be returned. (default: |FALSE|) ---           Not supported on Windows yet. ---+---   info  : If |TRUE|, return a list of Dicts with detailed info+---           instead of addresses. Implies `peer=true`. Each Dict+---           has the following items:+---             addr   (string)  Server address.+---             pid    (number?) PID of the Nvim process owning the+---                              server, or |v:null| if the peer is+---                              unreachable.+---             own    (bool)    Whether this server belongs to the+---                              current Nvim instance.+---           (default: |FALSE|)+--- --- Example: >vim ---   echo serverlist()+---   echo serverlist(#{info: v:true}) --- < --- --- @param opts? table---- @return string[]+--- @return string[]|vim.ServerInfo[] function vim.fn.serverlist(opts) end  --- Opens a socket or named pipe at {address} and listens for
src/nvim/eval.lua +13 −1
@@ -9971,13 +9971,25 @@ M.funcs = {                 will also be returned. (default: |FALSE|)                 Not supported on Windows yet. +        info  : If |TRUE|, return a list of Dicts with detailed info+                instead of addresses. Implies `peer=true`. Each Dict+                has the following items:+                  addr   (string)  Server address.+                  pid    (number?) PID of the Nvim process owning the+                                   server, or |v:null| if the peer is+                                   unreachable.+                  own    (bool)    Whether this server belongs to the+                                   current Nvim instance.+                (default: |FALSE|)+       Example: >vim       	echo serverlist()+      	echo serverlist(#{info: v:true})       <     ]=],     name = 'serverlist',     params = { { 'opts', 'table' } },-    returns = 'string[]',+    returns = 'string[]|vim.ServerInfo[]',     signature = 'serverlist([{opts}])',   },   serverstart = {
test/functional/core/server_spec.lua +31 −0
@@ -216,6 +216,37 @@ describe('server', function()     eq(true, vim.list_contains(new_servs, peer_addr))     eq(true, #servers_without_peer < #new_servs)     eq(true, old_servs_num < #new_servs)++    -- serverlist({info=true}) returns rich info dicts (implies peer=true)+    local info_servs = fn.serverlist({ info = true })+    eq(#new_servs, #info_servs)++    local own_addr = fn.serverlist()[1]+    local self_pid = fn.getpid()+    ---@type table<string, table>+    local by_addr = {}+    for _, entry in ipairs(info_servs) do+      eq('string', type(entry.addr))+      eq('boolean', type(entry.own))+      by_addr[entry.addr] = entry+    end++    -- own server: own=true, pid matches+    local own_entry = by_addr[own_addr]+    eq(true, own_entry ~= nil)+    eq(true, own_entry.own)+    eq(self_pid, own_entry.pid)++    -- peer server: own=false, pid is from peer process+    local peer_entry = by_addr[peer_addr]+    eq(true, peer_entry ~= nil)+    eq(false, peer_entry.own)+    eq('number', type(peer_entry.pid))+    n.set_session(client)+    local peer_pid = fn.getpid()+    n.set_session(current_server)+    eq(peer_pid, peer_entry.pid)+     client:close()   end) 

discussion · on the pull request

v1 · 8 messages

szymonwilczek author 25 Apr 2026

feat(server): return detailed info from serverlist({info=true})

szymonwilczek author 25 Apr 2026

Looks like test flaked on TUI :restart. I think I don't have permission (or at least do not see the re-run CI jobs button) from my fork, could someone give it a kick? Edit: Not longer needed, I had to update the docs nevertheless

szymonwilczek author 25 Apr 2026

@justinmk CI is fully green. serverlist({info=v:true}) implementation is complete, I've covered both local sockets and cross-process PID retrieval, along with updated docs & tests. Whenever you have a cycle, let me know if this looks solid to you. Once this gets the green light I'll rebase the remote-ssh pr onto it and wire new dicts into the ui picker.

justinmk maintainer 26 Apr 2026

---@field own boolean True if this server belongs to the current Nvim instance.

justinmk maintainer 26 Apr 2026

---@field addr string Server address (socket path, named pipe, or TCP host:port).

szymonwilczek author 29 Apr 2026

I've rebased on master since new news.txt caused conflicts. I've also updated the message to reflect the actual implementation (address -> addr, is_current -> own). Edit: Regarding the Windows CI failure: it threw exit code and a bunch of EOFs from provider tests. I don't know much about Windows in development (not using), but this looks like a broken Windows runner or missing DLLs rather than an issue with Lua code. Could anyone confirm?

szymonwilczek author 6 May 2026

@justinmk Windows functional CI timed out (55m, unrelated provider/DLL failures as before). All other checks are green. Could you confirm if the Windows failure is some kind of known issue so I can re-request review?

justinmk maintainer 6 May 2026

Let's pull the trigger on this, can continue iterating. Thanks!
the whole thread on github.com
12/12
salina
aurum
herbarium
mysterium
parchment
whispergrove
M-x 0/0

keybindings & navigation

M-x / Alt+x Open Emacs command runner
Shift + W Jump to writings & essays (/writings)
Shift + R Jump to research & upstream work (/research)
Shift + T Choose colour theme (au-themes-select)
j / k Scroll down / up
g g Scroll to the top of page
Shift + G Scroll to the bottom of page
? Toggle this keybindings help cheat sheet
Esc / C-g Dismiss modal or close active menu