summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorOlivia Kinnear <git@superatomic.dev>2026-04-23 16:11:59 -0500
committerGitHub <noreply@github.com>2026-04-23 17:11:59 -0400
commit645a588aa60f4e816a704c97685e2036958af176 (patch)
treeddd9fb1d80b0c8b1f4e0ae54e86576492baf1acb
parentc42aea3d37d96cd4275847ee0a71aa270596ba7f (diff)
feat(excmd): add :uptime command #39331
Problem Nvim marks its v:starttime, but there is no user-friendly way to get Nvim's uptime. Solution Add :uptime (based loosely on uptime(1)).
-rw-r--r--runtime/doc/index.txt1
-rw-r--r--runtime/doc/news.txt1
-rw-r--r--runtime/doc/various.txt4
-rw-r--r--runtime/doc/vim_diff.txt1
-rw-r--r--runtime/lua/vim/_core/ex_cmd.lua14
-rw-r--r--runtime/lua/vim/_core/time.lua35
-rw-r--r--src/nvim/ex_cmds.lua6
-rw-r--r--src/nvim/ex_docmd.c6
-rw-r--r--test/functional/core/main_spec.lua1
-rw-r--r--test/functional/ex_cmds/uptime_spec.lua25
-rw-r--r--test/functional/lua/time_spec.lua42
11 files changed, 133 insertions, 3 deletions
diff --git a/runtime/doc/index.txt b/runtime/doc/index.txt
index 3d11d8502e..f7b0edaf04 100644
--- a/runtime/doc/index.txt
+++ b/runtime/doc/index.txt
@@ -1678,6 +1678,7 @@ Tag Command Action ~
|:unmenu| :unme[nu] remove menu
|:unsilent| :uns[ilent] run a command not silently
|:update| :up[date] write buffer if modified
+|:uptime| :upt[ime] show Nvim's uptime
|:vglobal| :v[global] execute commands for not matching lines
|:version| :ve[rsion] print version number and other info
|:verbose| :verb[ose] execute command with 'verbose' set
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 42e62a727c..6f70a00abe 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -128,6 +128,7 @@ EDITOR
• |gf| and |<cfile>| support `file://…` URIs.
• |:log| opens log files.
• |ZR| restarts Nvim (|:restart|).
+• |:uptime| displays uptime.
EVENTS
diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt
index 8008627511..4628b55a5b 100644
--- a/runtime/doc/various.txt
+++ b/runtime/doc/various.txt
@@ -573,6 +573,10 @@ g== Executes the current code block.
Works in |help| buffers.
+ *:upt* *:uptime*
+:upt[ime] Shows Nvim's uptime.
+ See also |v:starttime|.
+
==============================================================================
2. Using Vim like less or more *less*
diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt
index d738638575..67a87a3c4e 100644
--- a/runtime/doc/vim_diff.txt
+++ b/runtime/doc/vim_diff.txt
@@ -321,6 +321,7 @@ Commands:
- User commands can support |:command-preview| to show results as you type
- |:write| with "++p" flag creates parent directories.
- |:update| command writes new file buffers even when unmodified.
+- |:uptime|
Editor:
- |prompt-buffer| supports multiline input/paste, undo/redo, and o/O normal
diff --git a/runtime/lua/vim/_core/ex_cmd.lua b/runtime/lua/vim/_core/ex_cmd.lua
index cd33a11662..bff960e39d 100644
--- a/runtime/lua/vim/_core/ex_cmd.lua
+++ b/runtime/lua/vim/_core/ex_cmd.lua
@@ -1,6 +1,8 @@
local api = vim.api
local fs = vim.fs
+local time = require('vim._core.time')
local util = require('vim._core.util')
+local uv = vim.uv
local N_ = vim.fn.gettext
--- Parsed ex command arguments for builtin commands, passed from C via `nlua_call_excmd`.
@@ -157,7 +159,7 @@ local available_subcmds = vim.tbl_keys(actions)
--- Implements command: `:lsp {subcmd} {name}?`.
--- @param eap vim._core.ExCmdArgs
-M.ex_lsp = function(eap)
+function M.ex_lsp(eap)
local fargs = api.nvim_parse_cmd('lsp ' .. eap.args, {}).args
if not fargs then
return
@@ -198,7 +200,7 @@ local log_dir = vim.fn.stdpath('log')
--- Implements command: `:log {file}`.
--- @param eap vim._core.ExCmdArgs
-M.ex_log = function(eap)
+function M.ex_log(eap)
local filename = eap.args
if filename == '' then
util.wrapped_edit(log_dir, eap.smods)
@@ -236,7 +238,7 @@ end
--- `:terminal [cmd]`
--- @param eap vim._core.ExCmdArgs
--- @param shell_argv? string[] Tokenized 'shell' from C (shell_build_argv), for the no-cmd case.
-M.ex_terminal = function(eap, shell_argv)
+function M.ex_terminal(eap, shell_argv)
local smods = eap.smods
local has_mods = (smods.tab or 0) > 0
or (smods.split or '') ~= ''
@@ -256,4 +258,10 @@ M.ex_terminal = function(eap, shell_argv)
end
end
+function M.ex_uptime()
+ local uptime = math.floor((uv.hrtime() - vim.v.starttime) / 1e9)
+ local uptime_display = time.fmt_rtime(uptime)
+ api.nvim_echo({ { N_('Up %s'):format(uptime_display) } }, true, {})
+end
+
return M
diff --git a/runtime/lua/vim/_core/time.lua b/runtime/lua/vim/_core/time.lua
new file mode 100644
index 0000000000..b942147c31
--- /dev/null
+++ b/runtime/lua/vim/_core/time.lua
@@ -0,0 +1,35 @@
+local N_ = vim.fn.gettext
+
+local M = {}
+
+--- @param seconds_in_unit integer How many seconds make up the unit.
+--- @param singular string The singular name of the unit. ("1 second")
+--- @param fplural string The plural name of the unit, to format. ("%s seconds")
+--- @param times string[] Working list of uptime strings.
+--- @param remaining integer Remaining time, in seconds.
+--- @return integer remaining Remaining time.
+local function time_part(seconds_in_unit, singular, fplural, times, remaining)
+ local unit = math.floor(remaining / seconds_in_unit)
+ if unit ~= 0 or #times ~= 0 or seconds_in_unit == 1 then
+ local display = unit == 1 and singular or fplural:format(unit)
+ times[#times + 1] = display
+ end
+ return remaining % seconds_in_unit
+end
+
+--- Display seconds in a pretty form (e.g. "1 hour, 24 minutes, 13 seconds").
+---
+--- @param seconds integer Time in seconds.
+--- @return string time Pretty representation of the time.
+function M.fmt_rtime(seconds)
+ local times = {}
+ seconds = time_part(86400, N_('1 day'), N_('%s days'), times, seconds)
+ seconds = time_part(3600, N_('1 hour'), N_('%s hours'), times, seconds)
+ seconds = time_part(60, N_('1 minute'), N_('%s minutes'), times, seconds)
+ seconds = time_part(1, N_('1 second'), N_('%s seconds'), times, seconds)
+ assert(seconds == 0)
+
+ return table.concat(times, ', ')
+end
+
+return M
diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua
index 7f8efe90d1..25bba1eb17 100644
--- a/src/nvim/ex_cmds.lua
+++ b/src/nvim/ex_cmds.lua
@@ -3078,6 +3078,12 @@ M.cmds = {
func = 'ex_update',
},
{
+ command = 'uptime',
+ flags = bit.bor(CMDWIN, LOCK_OK),
+ addr_type = 'ADDR_NONE',
+ func = 'ex_uptime',
+ },
+ {
command = 'vglobal',
flags = bit.bor(RANGE, WHOLEFOLD, EXTRA, DFLALL, CMDWIN, LOCK_OK),
addr_type = 'ADDR_LINES',
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
index 0737e31642..3a761136c0 100644
--- a/src/nvim/ex_docmd.c
+++ b/src/nvim/ex_docmd.c
@@ -8311,6 +8311,12 @@ static void ex_lsp(exarg_T *eap)
nlua_call_excmd("vim._core.ex_cmd", "ex_lsp", eap, &cmdmod, NULL);
}
+/// ":uptime"
+static void ex_uptime(exarg_T *eap)
+{
+ nlua_call_excmd("vim._core.ex_cmd", "ex_uptime", eap, &cmdmod, NULL);
+}
+
/// ":fclose"
static void ex_fclose(exarg_T *eap)
{
diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua
index 50dfcd97ff..474519b16e 100644
--- a/test/functional/core/main_spec.lua
+++ b/test/functional/core/main_spec.lua
@@ -234,6 +234,7 @@ describe('vim._core', function()
'vim._core.stringbuffer',
'vim._core.system',
'vim._core.table',
+ 'vim._core.time',
'vim._core.ui2',
'vim._core.util',
'vim._core.vimfn',
diff --git a/test/functional/ex_cmds/uptime_spec.lua b/test/functional/ex_cmds/uptime_spec.lua
new file mode 100644
index 0000000000..638ca16639
--- /dev/null
+++ b/test/functional/ex_cmds/uptime_spec.lua
@@ -0,0 +1,25 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local clear = n.clear
+local exec_capture = n.exec_capture
+local exec_lua = n.exec_lua
+local matches = t.matches
+
+describe(':uptime', function()
+ it('works', function()
+ clear()
+ matches([[Up %d+ seconds?]], exec_capture('uptime'))
+ end)
+
+ it('works without runtime', function()
+ clear {
+ args_rm = { '-u' },
+ args = { '-u', 'NONE' },
+ env = { VIMRUNTIME = 'non-existent' },
+ }
+ exec_lua(function()
+ vim.cmd('uptime')
+ end)
+ end)
+end)
diff --git a/test/functional/lua/time_spec.lua b/test/functional/lua/time_spec.lua
new file mode 100644
index 0000000000..a6d6f993dc
--- /dev/null
+++ b/test/functional/lua/time_spec.lua
@@ -0,0 +1,42 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local clear = n.clear
+
+local exec_lua = n.exec_lua
+local eq = t.eq
+
+describe('vim._core.time', function()
+ it('pretty_rtime()', function()
+ clear()
+ local function fmt_rtime(seconds)
+ return exec_lua(function()
+ return require('vim._core.time').fmt_rtime(seconds)
+ end)
+ end
+
+ -- Singular/plural works
+ eq('1 second', fmt_rtime(1))
+ eq('2 seconds', fmt_rtime(2))
+ eq('1 minute, 2 seconds', fmt_rtime(62))
+ eq('2 minutes, 1 second', fmt_rtime(121))
+
+ -- 0 units are included only when trailing
+ -- Seconds are included while leading, as they are by themselves
+ eq('0 seconds', fmt_rtime(0))
+ eq('1 minute, 0 seconds', fmt_rtime(60))
+ eq('1 hour, 0 minutes, 0 seconds', fmt_rtime(3600))
+ eq('1 day, 0 hours, 0 minutes, 0 seconds', fmt_rtime(86400))
+
+ -- Some random times
+ eq('1 hour, 6 minutes, 18 seconds', fmt_rtime(3978))
+ eq('7 hours, 8 minutes, 1 second', fmt_rtime(25681))
+ eq('3 days, 0 hours, 1 minute, 17 seconds', fmt_rtime(259277))
+
+ -- A second before a day
+ eq('23 hours, 59 minutes, 59 seconds', fmt_rtime(86399))
+
+ -- One year
+ eq('365 days, 0 hours, 0 minutes, 0 seconds', fmt_rtime(31536000))
+ end)
+end)