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 || 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 @@ -3077,6 +3077,12 @@ M.cmds = { addr_type = 'ADDR_LINES', 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), 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)