feat(server): add v:useractive, use it in serverlist(info=true) #39423

Problem:
When showing the :connect menu, it is useful to know which servers
are most-recently active. But we don't have a good way to detect that.

Solution:
- Introduce `v:useractive`.
- Include this timestamp in `serverlist({info=true})`.

Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
Signed-off-by: Szymon Wilczek <swilczek.lx@gmail.com>
This commit is contained in:
Szymon Wilczek
2026-05-07 16:47:51 +02:00
committed by GitHub
parent 97a557bd1e
commit 3639f7a867
12 changed files with 82 additions and 21 deletions

View File

@@ -241,6 +241,7 @@ VIMSCRIPT
• |v:exitreason| is set before |QuitPre|.
• |v:starttime| is the process start time (nanoseconds since UNIX epoch).
• |v:useractive| timestamp of the most recent user activity.
• |serverlist()| with `info=true` returns details for each server (own + peers).
==============================================================================

View File

@@ -9142,12 +9142,11 @@ serverlist([{opts}]) *serverlist()*
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.
addr (string) Server address.
pid (number) PID of the Nvim process.
own (bool) Whether this server belongs to
the current Nvim instance.
active (number) |v:useractive| of the server.
(default: |FALSE|)
Example: >vim

View File

@@ -754,6 +754,14 @@ v:true
operator) and to one when used as a Number (e.g. in |expr5| or
|expr7| when used with numeric operators). Read-only.
*v:useractive* *useractive-variable*
v:useractive
Timestamp indicating the most recent user activity.
Updated every time a key is received from a UI
(TUI keystrokes or RPC |nvim_input()|).
Initialized to 0 (no user activity since startup).
Read-only.
*v:val* *val-variable*
v:val
Value of the current item of a |List| or |Dictionary|. Only

View File

@@ -6,8 +6,9 @@ local M = {}
---
---@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 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.
@@ -42,6 +43,7 @@ function M.serverlist(opts, addrs)
end, { path = root, type = 'socket', limit = math.huge })
local peer_pids = {} ---@type table<string, integer>
local peer_active = {} ---@type table<string, integer>
for _, socket in ipairs(socket_paths) do
if not own_set[socket] and not vim.list_contains(addrs, socket) then
@@ -57,6 +59,10 @@ function M.serverlist(opts, addrs)
if ok_pid and type(pid) == 'number' then
peer_pids[socket] = pid
end
local ok_la, la = pcall(vim.fn.rpcrequest, chan, 'nvim_get_vvar', 'useractive')
if ok_la and type(la) == 'number' then
peer_active[socket] = la
end
end
end
pcall(vim.fn.chanclose, chan)
@@ -69,6 +75,7 @@ function M.serverlist(opts, addrs)
end
local self_pid = vim.fn.getpid()
local self_active = vim.v.useractive
local result = {} ---@type vim.ServerInfo[]
for _, addr in ipairs(addrs) do
local own = own_set[addr] == true
@@ -76,6 +83,7 @@ function M.serverlist(opts, addrs)
addr = addr,
pid = own and self_pid or peer_pids[addr],
own = own,
active = own and self_active or peer_active[addr],
})
end
return result

View File

@@ -8292,12 +8292,11 @@ function vim.fn.searchpos(pattern, flags, stopline, timeout, skip) end
--- 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.
--- addr (string) Server address.
--- pid (number) PID of the Nvim process.
--- own (bool) Whether this server belongs to
--- the current Nvim instance.
--- active (number) |v:useractive| of the server.
--- (default: |FALSE|)
---
--- Example: >vim

View File

@@ -805,6 +805,14 @@ vim.v.throwpoint = ...
--- @type boolean
vim.v['true'] = ...
--- Timestamp indicating the most recent user activity.
--- Updated every time a key is received from a UI
--- (TUI keystrokes or RPC `nvim_input()`).
--- Initialized to 0 (no user activity since startup).
--- Read-only.
--- @type integer
vim.v.useractive = ...
--- Value of the current item of a `List` or `Dictionary`. Only
--- valid while evaluating the expression used with `map()` and
--- `filter()`. Read-only.

View File

@@ -9974,12 +9974,11 @@ M.funcs = {
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.
addr (string) Server address.
pid (number) PID of the Nvim process.
own (bool) Whether this server belongs to
the current Nvim instance.
active (number) |v:useractive| of the server.
(default: |FALSE|)
Example: >vim

View File

@@ -217,6 +217,7 @@ static struct vimvar {
VV(VV_VIRTNUM, "virtnum", VAR_NUMBER, VV_RO),
VV(VV_STARTTIME, "starttime", VAR_NUMBER, VV_RO),
VV(VV_EXITREASON, "exitreason", VAR_STRING, VV_RO),
VV(VV_USERACTIVE, "useractive", VAR_NUMBER, VV_RO),
};
#undef VV

View File

@@ -137,4 +137,5 @@ typedef enum {
VV_VIRTNUM,
VV_STARTTIME,
VV_EXITREASON,
VV_USERACTIVE,
} VimVarIndex;

View File

@@ -11,6 +11,10 @@
#include "nvim/autocmd.h"
#include "nvim/autocmd_defs.h"
#include "nvim/buffer_defs.h"
#include "nvim/eval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/eval/vars.h"
#include "nvim/eval_defs.h"
#include "nvim/event/loop.h"
#include "nvim/event/multiqueue.h"
#include "nvim/event/rstream.h"
@@ -276,6 +280,10 @@ size_t input_enqueue(uint64_t chan_id, String keys)
{
current_ui = chan_id;
if (keys.size > 0) {
set_vim_var_nr(VV_USERACTIVE, (varnumber_T)os_realtime());
}
const char *ptr = keys.data;
const char *end = ptr + keys.size;

View File

@@ -447,6 +447,16 @@ M.vars = {
encoding. See |multi-lang|.
]=],
},
useractive = {
type = 'integer',
desc = [=[
Timestamp indicating the most recent user activity.
Updated every time a key is received from a UI
(TUI keystrokes or RPC |nvim_input()|).
Initialized to 0 (no user activity since startup).
Read-only.
]=],
},
lc_time = {
type = 'string',
desc = [=[

View File

@@ -231,25 +231,44 @@ describe('server', function()
by_addr[entry.addr] = entry
end
-- own server: own=true, pid matches
-- own server: own=true, pid matches, active present
local own_entry = by_addr[own_addr]
eq(true, own_entry ~= nil)
eq(true, own_entry.own)
eq(self_pid, own_entry.pid)
eq('number', type(own_entry.active))
eq(eval('v:useractive'), own_entry.active)
-- peer server: own=false, pid is from peer process
-- peer server: own=false, pid + active 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))
eq('number', type(peer_entry.active))
n.set_session(client)
local peer_pid = fn.getpid()
local peer_active = eval('v:useractive')
n.set_session(current_server)
eq(peer_pid, peer_entry.pid)
eq(peer_active, peer_entry.active)
client:close()
end)
it('v:useractive advances on UI input', function()
clear()
local before = eval('v:useractive')
eq('number', type(before))
n.feed('ix<Esc>')
local after = eval('v:useractive')
eq(true, after > before)
end)
it('v:useractive is read-only', function()
clear()
matches('E46:', pcall_err(n.command, 'let v:useractive = 0'))
end)
it('removes stale socket files automatically #36581', function()
-- Windows named pipes are ephemeral kernel objects that are automatically
-- cleaned up when the process terminates. Unix domain sockets persist as