When writing a Neovim configuration or plugin, it sometimes can become annoying when trying to debug an issue. The
available debug utilities are either print()
or vim.notify()
. In both cases (by default vim.notify()
is the same
as print()
), the log messes are written to :messages
, which does not allow to further select or process the printed
text (as far as I'm aware).
Setup
However, thanks to the flexibility of Neovim we can write our own logging function, which writes the output to a temporary logging buffer. A rough outline of such a function could be
--- @class LogOptions
--- @field namespace string namespace the log belongs to
local M = {}
--- @param msg string|table<string> log message (either single line or array
--- of lines to accept vim.inspect() output)
--- @param level integer|nil log level defined in vim.log.levels
--- @param options LogOptions allows us to set different logging namespaces
M.log = function(msg, level, options)
-- extracting a namespace to determine which buffer to log to
local opts = options or {}
local ns = opts.namespace or "default"
-- find the corresponding buffer and if there is no such buffer, create one
local buffer_name = M.buffer_name(ns)
local buffer = M.find_log_buffer(buffer_name)
if buffer == nil then
buffer = vim.api.nvim_create_buf(true, true)
vim.api.nvim_buf_set_name(buffer, buffer_name)
end
-- transform the integer log level to its string representation.
local level_str = "INFO"
for l, i in pairs(vim.log.levels) do
if level == i then
level_str = l
end
end
-- ensure `msg` is always a table to make processing simpler
if type(msg) == "string" then
msg = {msg}
end
-- Split `msg` on newlines, since nvim_buf_set_lines() does not like them
msg = vim.tbl_map(function(line)
return vim.split(line, "\n")
end, msg)
msg = vim.iter(msg):flatten(1):totable()
-- for each line add the log level
local complete_msg = vim.tbl_map(function (line)
return "[" .. level_str .. "] " .. line
end, msg)
-- actually add the lines to the buffer
vim.api.nvim_buf_set_lines(buffer, -1, -1, true, complete_msg)
end
--- @param namespace string
M.buffer_name = function (namespace)
return "LOG-" .. namespace
end
--- @param buffer_name string
M.find_log_buffer = function(buffer_name)
local buffer_list = vim.api.nvim_list_bufs()
for _, buf_num in ipairs(buffer_list) do
local name = vim.fn.bufname(buf_num)
if name == buffer_name then
return buf_num
end
end
return nil
end
--- @param namespace string
M.create_logger = function(namespace)
local opts = {namespace = namespace}
local logging_func_for = function(level)
return function(msg)
M.log(msg, level, opts)
end
end
return {
trace = logging_func_for(vim.log.levels.TRACE),
debug = logging_func_for(vim.log.levels.DEBUG),
info = logging_func_for(vim.log.levels.INFO),
warn = logging_func_for(vim.log.levels.WARN),
error = logging_func_for(vim.log.levels.ERROR),
}
end
This can directly be tested via
:lua require("myFileName").log("Some log message", vim.log.levels.INFO, {namespace="myNameSpace"})
which should create a new buffer called LOG-myNameSpace
with the content [INFO] Some log message
.
The above is just a rough idea, and could be customized further. I.e. rather than having different buffers per
namespace, it could log to a single one and prefix the messages with the namespace. Or use print()
if a namespace is
not set. You get the idea.
How to use
There are two ways on how to use this:
- use the log function directly in our plugin
- or replace the existing
vim.notify()
function with our logging function.
The first option is straight forward and ensures that no other plugin will use the custom logging function:
-- create a namespaced logger
local log = require("myFileName").create_logger("myNameSpace")
-- use it
log.info("Hello World")
log.error("That's not right")
log.debug(vim.inspect(vim.lsp.buf))
-- content of the corresponding LOG-myNameSpace buffer
-- [INFO] Hello World
-- [ERROR] That's not right
-- [DEBUG] {
-- [DEBUG] add_workspace_folder = <function 1>,
-- [DEBUG] clear_references = <function 2>,
-- [DEBUG] code_action = <function 3>,
-- [DEBUG] completion = <function 4>,
-- [DEBUG] declaration = <function 5>,
-- [DEBUG] definition = <function 6>,
-- [DEBUG] document_highlight = <function 7>,
-- [DEBUG] document_symbol = <function 8>,
-- [DEBUG] execute_command = <function 9>,
-- [DEBUG] format = <function 10>,
-- [DEBUG] hover = <function 11>,
-- [DEBUG] implementation = <function 12>,
-- [DEBUG] incoming_calls = <function 13>,
-- [DEBUG] list_workspace_folders = <function 14>,
-- [DEBUG] outgoing_calls = <function 15>,
-- [DEBUG] references = <function 16>,
-- [DEBUG] remove_workspace_folder = <function 17>,
-- [DEBUG] rename = <function 18>,
-- [DEBUG] signature_help = <function 19>,
-- [DEBUG] type_definition = <function 20>,
-- [DEBUG] typehierarchy = <function 21>,
-- [DEBUG] workspace_symbol = <function 22>
-- [DEBUG] }
The second option can be done by assigning the log function
vim.notify = require("myFileName").log
somewhere during the initialization of your lua dotfiles. It has the interesting side effect that other plugins using
vim.notify()
now also log into the temporary buffer.