From 3639f7a8672bfbc6844f94c1db98c06cc9649afc Mon Sep 17 00:00:00 2001 From: Szymon Wilczek Date: Thu, 7 May 2026 16:47:51 +0200 Subject: [PATCH] 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 Signed-off-by: Szymon Wilczek --- runtime/doc/news.txt | 1 + runtime/doc/vimfn.txt | 11 +++++------ runtime/doc/vvars.txt | 8 ++++++++ runtime/lua/vim/_core/server.lua | 10 +++++++++- runtime/lua/vim/_meta/vimfn.gen.lua | 11 +++++------ runtime/lua/vim/_meta/vvars.gen.lua | 8 ++++++++ src/nvim/eval.lua | 11 +++++------ src/nvim/eval/vars.c | 1 + src/nvim/eval_defs.h | 1 + src/nvim/os/input.c | 8 ++++++++ src/nvim/vvars.lua | 10 ++++++++++ test/functional/core/server_spec.lua | 23 +++++++++++++++++++++-- 12 files changed, 82 insertions(+), 21 deletions(-) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 42331101b2..fca2cee2ef 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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). ============================================================================== diff --git a/runtime/doc/vimfn.txt b/runtime/doc/vimfn.txt index 8e100af32e..59c73fa0a6 100644 --- a/runtime/doc/vimfn.txt +++ b/runtime/doc/vimfn.txt @@ -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 diff --git a/runtime/doc/vvars.txt b/runtime/doc/vvars.txt index e61bc78624..50036044fc 100644 --- a/runtime/doc/vvars.txt +++ b/runtime/doc/vvars.txt @@ -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 diff --git a/runtime/lua/vim/_core/server.lua b/runtime/lua/vim/_core/server.lua index 785a3bbd72..45fc262376 100644 --- a/runtime/lua/vim/_core/server.lua +++ b/runtime/lua/vim/_core/server.lua @@ -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 + local peer_active = {} ---@type table 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 diff --git a/runtime/lua/vim/_meta/vimfn.gen.lua b/runtime/lua/vim/_meta/vimfn.gen.lua index 1f381a7ac8..2cda894344 100644 --- a/runtime/lua/vim/_meta/vimfn.gen.lua +++ b/runtime/lua/vim/_meta/vimfn.gen.lua @@ -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 diff --git a/runtime/lua/vim/_meta/vvars.gen.lua b/runtime/lua/vim/_meta/vvars.gen.lua index 543d4169c1..3f90cdecdd 100644 --- a/runtime/lua/vim/_meta/vvars.gen.lua +++ b/runtime/lua/vim/_meta/vvars.gen.lua @@ -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. diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 4361f8ab0f..3aac69222a 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -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 diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index b2f300e5d1..03c252ee83 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -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 diff --git a/src/nvim/eval_defs.h b/src/nvim/eval_defs.h index 7cec3d6167..6a1781d05b 100644 --- a/src/nvim/eval_defs.h +++ b/src/nvim/eval_defs.h @@ -137,4 +137,5 @@ typedef enum { VV_VIRTNUM, VV_STARTTIME, VV_EXITREASON, + VV_USERACTIVE, } VimVarIndex; diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index 6436991aa8..e042ffdb87 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -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; diff --git a/src/nvim/vvars.lua b/src/nvim/vvars.lua index db87bcda28..63b7951b0e 100644 --- a/src/nvim/vvars.lua +++ b/src/nvim/vvars.lua @@ -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 = [=[ diff --git a/test/functional/core/server_spec.lua b/test/functional/core/server_spec.lua index 83f42b97e7..4bdb792fa0 100644 --- a/test/functional/core/server_spec.lua +++ b/test/functional/core/server_spec.lua @@ -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') + 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