mirror of
https://github.com/neovim/neovim.git
synced 2026-04-17 13:03:03 +00:00
feat(ex): add :log command
This commit is contained in:
@@ -1733,4 +1733,27 @@ mark a file as trusted or untrusted using the |:trust| command or the
|
||||
or |vim.secure.read()|, the user will be prompted to
|
||||
trust or deny it.
|
||||
|
||||
==============================================================================
|
||||
13. Log Files *log-files*
|
||||
|
||||
Nvim keeps log files in `stdpath("log")`. See |standard-path| for where that
|
||||
location is on your system.
|
||||
|
||||
*:log*
|
||||
:log [logname]
|
||||
Open a log file that exists in `stdpath("log")`.
|
||||
`[logname]` is the name of the file with the extension
|
||||
".log" removed.
|
||||
|
||||
Without a provided `[logname]`, opens the log
|
||||
directory.
|
||||
|
||||
Examples: >
|
||||
:log nvim
|
||||
< Open the general |log| file for Nvim. >
|
||||
:log lsp
|
||||
< Open the |lsp-log| file. >
|
||||
:log nvim-pack
|
||||
< Open the |vim.pack| log file.
|
||||
|
||||
vim:tw=78:ts=8:noet:ft=help:norl:
|
||||
|
||||
@@ -1415,6 +1415,7 @@ Tag Command Action ~
|
||||
|:loadview| :lo[adview] load view for current window from a file
|
||||
|:lockmarks| :loc[kmarks] following command keeps marks where they are
|
||||
|:lockvar| :lockv[ar] lock variables
|
||||
|:log| :log view a log file
|
||||
|:lolder| :lol[der] go to older location list
|
||||
|:lopen| :lop[en] open location window
|
||||
|:lprevious| :lp[revious] go to previous location
|
||||
|
||||
@@ -2472,11 +2472,9 @@ of the LSP client RPC events. Example: >lua
|
||||
<
|
||||
|
||||
Then try to run the language server, and open the log with: >vim
|
||||
:lua vim.cmd('tabnew ' .. vim.lsp.log.get_filename())
|
||||
:log lsp
|
||||
<
|
||||
|
||||
(Or use `:LspLog` if you have nvim-lspconfig installed.)
|
||||
|
||||
Note:
|
||||
• Remember to DISABLE verbose logging ("debug" or "trace" level), else you may
|
||||
encounter performance issues.
|
||||
|
||||
@@ -78,7 +78,7 @@ DIAGNOSTICS
|
||||
|
||||
EDITOR
|
||||
|
||||
• todo
|
||||
• |:log| for opening log files
|
||||
|
||||
EVENTS
|
||||
|
||||
|
||||
@@ -1449,10 +1449,14 @@ To run Nvim without creating any directories or data files: >
|
||||
|
||||
LOG FILE *log* *$NVIM_LOG_FILE* *E5430*
|
||||
Besides 'debug' and 'verbose', Nvim keeps a general log file for internal
|
||||
debugging, plugins and RPC clients. >
|
||||
:echo $NVIM_LOG_FILE
|
||||
debugging, plugins and RPC clients. The log file can be viewed with: >
|
||||
:log nvim
|
||||
|
||||
Default location is stdpath("log")/nvim.log ($XDG_STATE_HOME/nvim/logs/nvim.log)
|
||||
unless that path is inaccessible or $NVIM_LOG_FILE was set before |startup|.
|
||||
|
||||
See |log-files| for details about other log files that can be created by Nvim
|
||||
during runtime or by plugins.
|
||||
|
||||
|
||||
vim:et:tw=78:ts=8:sw=4:ft=help:norl:
|
||||
|
||||
@@ -311,6 +311,7 @@ Commands:
|
||||
- |:EditQuery|
|
||||
- |:Inspect|
|
||||
- |:InspectTree|
|
||||
- |:log|
|
||||
- |:lsp|
|
||||
- |:Man| is available by default, with many improvements such as completion
|
||||
- |:match| can be invoked before highlight group is defined
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
local api = vim.api
|
||||
local lsp = vim.lsp
|
||||
local fs = vim.fs
|
||||
local util = require('vim._core.util')
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -11,7 +12,7 @@ end
|
||||
--- @return string[]
|
||||
local function get_client_names()
|
||||
return vim
|
||||
.iter(lsp.get_clients())
|
||||
.iter(vim.lsp.get_clients())
|
||||
:map(function(client)
|
||||
return client.name
|
||||
end)
|
||||
@@ -24,7 +25,7 @@ end
|
||||
local function filtered_config_names(filter)
|
||||
return function()
|
||||
return vim
|
||||
.iter(lsp.get_configs(filter))
|
||||
.iter(vim.lsp.get_configs(filter))
|
||||
:map(function(config)
|
||||
return config.name
|
||||
end)
|
||||
@@ -43,8 +44,8 @@ local complete_args = {
|
||||
--- @param enable? boolean
|
||||
local function checked_enable(names, enable)
|
||||
for _, name in ipairs(names) do
|
||||
if name:find('*') == nil and lsp.config[name] ~= nil then
|
||||
lsp.enable(name, enable)
|
||||
if name:find('*') == nil and vim.lsp.config[name] ~= nil then
|
||||
vim.lsp.enable(name, enable)
|
||||
else
|
||||
echo_err(("No client config named '%s'"):format(name))
|
||||
end
|
||||
@@ -56,7 +57,7 @@ local function ex_lsp_enable(config_names)
|
||||
-- Default to enabling all clients matching the filetype of the current buffer.
|
||||
if #config_names == 0 then
|
||||
local filetype = vim.bo.filetype
|
||||
for _, config in ipairs(lsp.get_configs()) do
|
||||
for _, config in ipairs(vim.lsp.get_configs()) do
|
||||
local filetypes = config.filetypes
|
||||
if filetypes == nil or vim.list_contains(filetypes, filetype) then
|
||||
table.insert(config_names, config.name)
|
||||
@@ -80,12 +81,12 @@ local function ex_lsp_disable(config_names)
|
||||
-- Default to disabling all clients attached to the current buffer.
|
||||
if #config_names == 0 then
|
||||
config_names = vim
|
||||
.iter(lsp.get_clients { bufnr = api.nvim_get_current_buf() })
|
||||
.iter(vim.lsp.get_clients { bufnr = api.nvim_get_current_buf() })
|
||||
:map(function(client)
|
||||
return client.name
|
||||
end)
|
||||
:filter(function(name)
|
||||
return lsp.config[name] ~= nil
|
||||
return vim.lsp.config[name] ~= nil
|
||||
end)
|
||||
:totable()
|
||||
if #config_names == 0 then
|
||||
@@ -102,7 +103,7 @@ end
|
||||
local function get_clients_from_names(client_names)
|
||||
-- Default to all active clients attached to the current buffer.
|
||||
if #client_names == 0 then
|
||||
local clients = lsp.get_clients { bufnr = api.nvim_get_current_buf() }
|
||||
local clients = vim.lsp.get_clients { bufnr = api.nvim_get_current_buf() }
|
||||
if #clients == 0 then
|
||||
echo_err('No clients attached to current buffer')
|
||||
end
|
||||
@@ -111,7 +112,7 @@ local function get_clients_from_names(client_names)
|
||||
return vim
|
||||
.iter(client_names)
|
||||
:map(function(name)
|
||||
local clients = lsp.get_clients { name = name }
|
||||
local clients = vim.lsp.get_clients { name = name }
|
||||
if #clients == 0 then
|
||||
echo_err(("No active clients named '%s'"):format(name))
|
||||
end
|
||||
@@ -186,4 +187,45 @@ function M.lsp_complete(line)
|
||||
end
|
||||
end
|
||||
|
||||
--- @type string
|
||||
--- @diagnostic disable-next-line: assign-type-mismatch
|
||||
local log_dir = vim.fn.stdpath('log')
|
||||
|
||||
--- Implements command: `:log {file}`.
|
||||
--- @param filename string
|
||||
--- @param mods string
|
||||
M.ex_log = function(filename, mods)
|
||||
if filename == '' then
|
||||
util.wrapped_edit(log_dir, mods)
|
||||
else
|
||||
local path --- @type string
|
||||
-- Special case for NVIM_LOG_FILE
|
||||
local nvim_log_file = vim.env.NVIM_LOG_FILE --- @type string
|
||||
if filename == 'nvim' and nvim_log_file and nvim_log_file ~= '' then
|
||||
path = nvim_log_file
|
||||
else
|
||||
path = fs.joinpath(log_dir, filename .. '.log')
|
||||
end
|
||||
if not vim.uv.fs_stat(path) then
|
||||
echo_err(("No such log file: '%s'"):format(path))
|
||||
return
|
||||
end
|
||||
util.wrapped_edit(path, mods)
|
||||
vim.cmd.normal { 'G', bang = true }
|
||||
end
|
||||
end
|
||||
|
||||
--- Completion logic for `:log` command
|
||||
--- @return string[] list of completions
|
||||
function M.log_complete()
|
||||
local names = { 'nvim' } --- @type string[]
|
||||
for file, type in vim.fs.dir(log_dir, { depth = math.huge }) do
|
||||
local name, matches = file:gsub('%.log$', '')
|
||||
if matches ~= 0 and type == 'file' and name ~= 'nvim' then
|
||||
names[#names + 1] = name
|
||||
end
|
||||
end
|
||||
return names
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -22,6 +22,17 @@ function M.space_below()
|
||||
add_blank()
|
||||
end
|
||||
|
||||
--- Gets a buffer by name
|
||||
--- @param name string
|
||||
--- @return integer?
|
||||
function M.get_buf_by_name(name)
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
if vim.api.nvim_buf_get_name(buf) == name then
|
||||
return buf
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Edit a file in a specific window
|
||||
--- @param winnr number
|
||||
--- @param file string
|
||||
@@ -49,6 +60,23 @@ M.edit_in = function(winnr, file)
|
||||
end)
|
||||
end
|
||||
|
||||
--- :edit, but it respects commands like :hor, :vert, :tab, etc.
|
||||
--- @param file string
|
||||
--- @param mods_str string
|
||||
function M.wrapped_edit(file, mods_str)
|
||||
local cmdline = table.concat({ mods_str, 'edit' }, ' ')
|
||||
local mods = vim.api.nvim_parse_cmd(cmdline, {}).mods
|
||||
--- @diagnostic disable-next-line: need-check-nil
|
||||
if mods.tab > 0 or mods.split ~= '' or mods.horizontal or mods.vertical then
|
||||
local buf = M.get_buf_by_name(file)
|
||||
if buf == nil then
|
||||
buf = vim.api.nvim_create_buf(true, false)
|
||||
end
|
||||
vim.cmd.sbuffer { buf, mods = mods }
|
||||
end
|
||||
vim.cmd.edit { file }
|
||||
end
|
||||
|
||||
--- Read a chunk of data from a file
|
||||
--- @param file string
|
||||
--- @param size number
|
||||
|
||||
@@ -10,11 +10,9 @@
|
||||
---
|
||||
--- Then try to run the language server, and open the log with:
|
||||
--- ```vim
|
||||
--- :lua vim.cmd('tabnew ' .. vim.lsp.log.get_filename())
|
||||
--- :log lsp
|
||||
--- ```
|
||||
---
|
||||
--- (Or use `:LspLog` if you have nvim-lspconfig installed.)
|
||||
---
|
||||
--- Note:
|
||||
--- - Remember to DISABLE verbose logging ("debug" or "trace" level), else you may encounter
|
||||
--- performance issues.
|
||||
|
||||
@@ -1304,6 +1304,7 @@ char *addstar(char *fname, size_t len, int context)
|
||||
&& fname[0] == '/')
|
||||
|| context == EXPAND_CHECKHEALTH
|
||||
|| context == EXPAND_LSP
|
||||
|| context == EXPAND_LOG
|
||||
|| context == EXPAND_LUA) {
|
||||
retval = xstrnsave(fname, len);
|
||||
} else {
|
||||
@@ -2316,6 +2317,10 @@ static const char *set_context_by_cmdname(const char *cmd, cmdidx_T cmdidx, expa
|
||||
xp->xp_context = EXPAND_CHECKHEALTH;
|
||||
break;
|
||||
|
||||
case CMD_log:
|
||||
xp->xp_context = EXPAND_LOG;
|
||||
break;
|
||||
|
||||
case CMD_lsp:
|
||||
xp->xp_context = EXPAND_LSP;
|
||||
break;
|
||||
@@ -2858,6 +2863,43 @@ static char *get_healthcheck_names(expand_T *xp FUNC_ATTR_UNUSED, int idx)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/// Completion for |:log| command.
|
||||
///
|
||||
/// Given to ExpandGeneric() to obtain `:log` completion.
|
||||
/// @param[in] idx Index of the item.
|
||||
/// @param[in] xp Not used.
|
||||
static char *get_log_arg(expand_T *xp FUNC_ATTR_UNUSED, int idx)
|
||||
{
|
||||
static Object names = OBJECT_INIT;
|
||||
static char *last_xp_line = NULL;
|
||||
static unsigned last_gen = 0;
|
||||
|
||||
if (last_xp_line == NULL || strcmp(last_xp_line,
|
||||
xp->xp_line) != 0
|
||||
|| last_gen != get_cmdline_last_prompt_id()) {
|
||||
xfree(last_xp_line);
|
||||
last_xp_line = xstrdup(xp->xp_line);
|
||||
MAXSIZE_TEMP_ARRAY(args, 1);
|
||||
Error err = ERROR_INIT;
|
||||
|
||||
ADD_C(args, CSTR_AS_OBJ(xp->xp_line));
|
||||
// Build the current command line as a Lua string argument
|
||||
Object res = NLUA_EXEC_STATIC("return require'vim._core.ex_cmd'.log_complete(...)", args,
|
||||
kRetObject, NULL,
|
||||
&err);
|
||||
api_clear_error(&err);
|
||||
api_free_object(names);
|
||||
names = res;
|
||||
last_gen = get_cmdline_last_prompt_id();
|
||||
}
|
||||
|
||||
if (names.type == kObjectTypeArray && idx < (int)names.data.array.size
|
||||
&& names.data.array.items[idx].type == kObjectTypeString) {
|
||||
return names.data.array.items[idx].data.string.data;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/// Completion for |:lsp| command.
|
||||
///
|
||||
/// Given to ExpandGeneric() to obtain `:lsp` completion.
|
||||
@@ -2937,6 +2979,7 @@ static int ExpandOther(char *pat, expand_T *xp, regmatch_T *rmp, char ***matches
|
||||
{ EXPAND_SCRIPTNAMES, get_scriptnames_arg, true, false },
|
||||
{ EXPAND_RETAB, get_retab_arg, true, true },
|
||||
{ EXPAND_CHECKHEALTH, get_healthcheck_names, true, false },
|
||||
{ EXPAND_LOG, get_log_arg, true, false },
|
||||
{ EXPAND_LSP, get_lsp_arg, true, false },
|
||||
};
|
||||
int ret = FAIL;
|
||||
|
||||
@@ -119,6 +119,7 @@ enum {
|
||||
EXPAND_CHECKHEALTH,
|
||||
EXPAND_LUA,
|
||||
EXPAND_LSP,
|
||||
EXPAND_LOG,
|
||||
};
|
||||
|
||||
/// Type used by ExpandGeneric()
|
||||
|
||||
@@ -1586,6 +1586,12 @@ M.cmds = {
|
||||
addr_type = 'ADDR_NONE',
|
||||
func = 'ex_lockvar',
|
||||
},
|
||||
{
|
||||
command = 'log',
|
||||
flags = bit.bor(EXTRA, TRLBAR),
|
||||
addr_type = 'ADDR_NONE',
|
||||
func = 'ex_log',
|
||||
},
|
||||
{
|
||||
command = 'lolder',
|
||||
flags = bit.bor(RANGE, COUNT, TRLBAR),
|
||||
|
||||
@@ -8291,6 +8291,31 @@ static void ex_terminal(exarg_T *eap)
|
||||
do_cmdline_cmd(ex_cmd);
|
||||
}
|
||||
|
||||
/// ":log {name}"
|
||||
static void ex_log(exarg_T *eap)
|
||||
{
|
||||
Error err = ERROR_INIT;
|
||||
MAXSIZE_TEMP_ARRAY(args, 2);
|
||||
|
||||
char mods[1024];
|
||||
size_t mods_len = 0;
|
||||
mods[0] = NUL;
|
||||
|
||||
if (cmdmod.cmod_tab > 0 || cmdmod.cmod_split != 0) {
|
||||
bool multi_mods = false;
|
||||
mods_len = add_win_cmd_modifiers(mods, &cmdmod, &multi_mods);
|
||||
assert(mods_len < sizeof(mods));
|
||||
}
|
||||
ADD_C(args, CSTR_AS_OBJ(eap->arg));
|
||||
ADD_C(args, STRING_OBJ(((String){ .data = mods, .size = mods_len })));
|
||||
|
||||
NLUA_EXEC_STATIC("require'vim._core.ex_cmd'.ex_log(...)", args, kRetNilBool, NULL, &err);
|
||||
if (ERROR_SET(&err)) {
|
||||
emsg_multiline(err.msg, "lua_error", HLF_E, true);
|
||||
}
|
||||
api_clear_error(&err);
|
||||
}
|
||||
|
||||
/// ":lsp {subcmd} {clients}"
|
||||
static void ex_lsp(exarg_T *eap)
|
||||
{
|
||||
|
||||
77
test/functional/ex_cmds/log_spec.lua
Normal file
77
test/functional/ex_cmds/log_spec.lua
Normal file
@@ -0,0 +1,77 @@
|
||||
local t = require('test.testutil')
|
||||
local n = require('test.functional.testnvim')()
|
||||
|
||||
local clear = n.clear
|
||||
local eq = t.eq
|
||||
local exec_lua = n.exec_lua
|
||||
local is_os = t.is_os
|
||||
local pathsep = n.get_pathsep()
|
||||
local write_file = t.write_file
|
||||
|
||||
describe(':log', function()
|
||||
local xstate = 'Xstate'
|
||||
local name = (is_os('win') and 'nvim-data' or 'nvim')
|
||||
|
||||
before_each(function()
|
||||
clear { env = { XDG_STATE_HOME = xstate } }
|
||||
|
||||
local std_state = xstate .. pathsep .. name
|
||||
n.mkdir_p(std_state .. pathsep .. 'logs')
|
||||
write_file(std_state .. '/logs/nvim.log', [[nvim log]])
|
||||
write_file(std_state .. '/logs/foo.log', [[foo log]])
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
n.rmdir(xstate)
|
||||
end)
|
||||
|
||||
it('without argument opens log folder', function()
|
||||
eq(
|
||||
xstate .. '/' .. name .. '/logs',
|
||||
exec_lua(function()
|
||||
vim.cmd('log')
|
||||
return vim.fs.normalize(vim.fn.expand('%'))
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
it('with argument opens corresponding log file', function()
|
||||
eq(
|
||||
xstate .. '/' .. name .. '/logs/foo.log',
|
||||
exec_lua(function()
|
||||
vim.cmd('log foo')
|
||||
return vim.fs.normalize(vim.fn.expand('%'))
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
it('nvim works with non-default $NVIM_LOG_FILE', function()
|
||||
clear { env = { XDG_STATE_HOME = xstate, NVIM_LOG_FILE = 'Xfoo.log' } }
|
||||
write_file('Xfoo.log', [[nvim log]])
|
||||
eq(
|
||||
'Xfoo.log',
|
||||
exec_lua(function()
|
||||
vim.cmd('log nvim')
|
||||
return vim.fn.expand('%')
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
it('argument completion', function()
|
||||
local completions = exec_lua(function()
|
||||
return vim.fn.getcompletion('log ', 'cmdline')
|
||||
end)
|
||||
eq({ 'foo', 'nvim' }, completions)
|
||||
end)
|
||||
|
||||
it('works without runtime', function()
|
||||
clear {
|
||||
args_rm = { '-u' },
|
||||
args = { '-u', 'NONE' },
|
||||
env = { VIMRUNTIME = 'non-existent' },
|
||||
}
|
||||
exec_lua(function()
|
||||
vim.cmd('log')
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
Reference in New Issue
Block a user