├── LICENSE ├── README.md ├── dlog.lua ├── doc └── debuglog.txt └── lua └── debuglog.lua /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 smartpde 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # debuglog for Neovim plugin developers 2 | 3 | `debuglog` is made for Neovim plugin developers to debug the plugin locally, or collect 4 | debugging info from users. 5 | 6 | With `debuglog`, you leave the debug statements in the code. By default, nothing is logged, 7 | but you can enable the loggers selectively. 8 | 9 | The users of your plugin do _not_ need the `debuglog` to be installed. All logging is done 10 | through the tiny [shim](#shim) file that you include in your plugin. Without the full 11 | `debuglog` module, everything is a no-op. 12 | 13 | ## Installation 14 | 15 | To install the plugin with [packer.nvim](https://github.com/wbthomason/packer.nvim): 16 | 17 | ```lua 18 | use {"smartpde/debuglog"} 19 | ``` 20 | 21 | Once installed, call the `setup()` function to register various commands: 22 | ```lua 23 | require("debuglog").setup() 24 | ``` 25 | 26 | ## Shim 27 | 28 | `debuglog` is an optional dependency of your plugin, therefore you must install the tiny 29 | shim file [dlog.lua](https://github.com/smartpde/debuglog/blob/main/dlog.lua) into your 30 | plugin's directory. The shim checks if the full `debuglog` module is present, and turns 31 | all logging into a no-op otherwise. 32 | 33 | There is a simple command to copy the shim file for you: 34 | 35 | ``` 36 | :DebugLogInstallShim 37 | 38 | For example: 39 | :DebugLogInstallShim ~/projects/my_awesome_plugin/lua 40 | ``` 41 | 42 | You can of course copy the file any other way. 43 | 44 | ## Writing debug statements 45 | 46 | Note that all debug loggers must be created using the `dlog` shim module: 47 | 48 | ```lua 49 | -- Enable logging by running ":DebugLogEnable *" command first. 50 | 51 | local dlog = require("dlog") 52 | local logger1 = dlog.logger("some_logger") 53 | local logger2 = dlog.logger("another_logger") 54 | 55 | logger1("This is from %s", "some_logger") 56 | logger1("This is also from %s", "some_logger") 57 | logger2("And this is from %s", "another_logger") 58 | 59 | -- you can also check if the logger is enabled if the value to print 60 | -- is expensive to get 61 | if dlog.is_enabled("logger1") then 62 | logger1("Print some heavy string: %s", "heavy string") 63 | end 64 | ``` 65 | 66 | You can create many named loggers, the logger name will be attached to all its 67 | messages. Additionally, log statements in the vim `:messages` will be nicely colored 68 | for easier identification: 69 | 70 | image 71 | 72 | The loggers use standard Lua's [string.format()](https://www.lua.org/pil/20.html). 73 | 74 | ## Enable and disable logging 75 | 76 | By default, nothing is logged unless you enable the loggers. To do so you can use the plugin 77 | commands: 78 | 79 | - Enable all loggers: 80 | 81 | ``` 82 | :DebugLogEnable * 83 | ``` 84 | 85 | - Enable loggers selectively: 86 | 87 | ``` 88 | :DebugLogEnable some_logger,another_logger 89 | ``` 90 | 91 | Note that enabling new loggers disables all previous ones. 92 | 93 | - Disable all logging: 94 | 95 | ``` 96 | :DebugLogDisable 97 | ``` 98 | 99 | You can also use the corresponding Lua functions: 100 | 101 | ```lua 102 | local debuglog = require("debuglog") 103 | debuglog.enable("*") 104 | debuglog.disable() 105 | ``` 106 | 107 | ## Logging to file 108 | 109 | By default, logs are written only to the `:messages` console. You can also enable logging 110 | to a file: 111 | 112 | - With user command: 113 | 114 | ``` 115 | :DebugLogEnableFileLogging 116 | ``` 117 | 118 | - With Lua: 119 | 120 | ```lua 121 | require("debuglog").set_config({ 122 | log_to_file = true, 123 | log_to_console = true, 124 | }) 125 | ``` 126 | 127 | The log file path will be printed out. You can also use the following command 128 | open the log file in Neovim: 129 | 130 | ``` 131 | :DebugLogOpenFileLog 132 | ``` 133 | 134 | Or get the log file path in Lua: 135 | 136 | ```lua 137 | require("debuglog").log_file_path() 138 | ``` 139 | 140 | To disable file logging run: 141 | ``` 142 | :DebugLogDisableFileLogging 143 | ``` 144 | 145 | ## Configuration options 146 | 147 | The full list of configuration options with their defaults: 148 | 149 | ```lua 150 | require("debuglog").setup({ 151 | log_to_console = true, 152 | log_to_file = false, 153 | -- The highlight group for printing the time column in console 154 | time_hl_group = "Comment", 155 | }) 156 | ``` 157 | 158 | The options can also be modified by calling the `set_config(opts)` function 159 | after the setup. 160 | -------------------------------------------------------------------------------- /dlog.lua: -------------------------------------------------------------------------------- 1 | ---dlog is a module for writing debug logs. 2 | --- 3 | ---WARNING: This file is auto-generated, DO NOT MODIFY. 4 | --- 5 | ---Example usage: 6 | --- local d = require("dlog").logger("my_logger") 7 | --- d("Formatted lua string %s, number %d, etc", "test", 42) 8 | --- 9 | ---This will print "Formatted lua string test, number 42, etc" 10 | --- 11 | ---If debuglog plugin is not installed, all logs are no-op. 12 | ---Read more at https://github.com/smartpde/debuglog#shim 13 | local has_debuglog, debuglog = pcall(require, "debuglog") 14 | 15 | local M = {} 16 | 17 | local function noop(_) 18 | end 19 | 20 | ---Returns the logger object if the debuglog plugin installed, or a 21 | ---no-op function otherwise. 22 | ---@param logger_name string the name of the logger 23 | ---@return fun(msg: string, ...): any logger function 24 | function M.logger(logger_name) 25 | if has_debuglog then 26 | return debuglog.logger_for_shim_only(logger_name) 27 | end 28 | return noop 29 | end 30 | 31 | ---Checks if the logger is enabled. 32 | ---@param logger_name string the name of the logger 33 | ---@return boolean enabled whether the logger is enabled 34 | function M.is_enabled(logger_name) 35 | if has_debuglog then 36 | return debuglog.is_enabled(logger_name) 37 | end 38 | return false 39 | end 40 | 41 | return M 42 | -------------------------------------------------------------------------------- /doc/debuglog.txt: -------------------------------------------------------------------------------- 1 | *debuglog* for Neovim plugin developers. 2 | 3 | debuglog is made for Neovim plugin developers to debug the plugin locally, or 4 | collect debugging info from users. 5 | 6 | With debuglog, you leave the debug statements in the code. By default, nothing 7 | is logged, but you can enable the loggers selectively. 8 | 9 | The users of your plugin do _not_ need the debuglog to be installed. All logging 10 | is done through the tiny |debuglog-shim| file that you include in your plugin. 11 | Without the full debuglog module, everything is a no-op. 12 | 13 | ============================================================================== 14 | CONTENTS *debuglog-contents* 15 | 16 | Shim |debuglog-shim| 17 | Writing debug statements |debuglog-write| 18 | User commands |debuglog-commands| 19 | Lua API |debuglog-lua| 20 | 21 | ============================================================================== 22 | Shim *debuglog-shim* 23 | 24 | debuglog is an optional dependency of your plugin, therefore you must install 25 | the tiny shim file dlog.lua into your plugin's directory. The shim checks if the 26 | full debuglog module is present, and turns all logging into a no-op otherwise. 27 | 28 | There is a simple command to copy the shim file for you: 29 | 30 | > 31 | :DebugLogInstallShim 32 | 33 | For example: 34 | :DebugLogInstallShim ~/projects/my_awesome_plugin/lua 35 | < 36 | 37 | You can of course copy the file any other way. 38 | 39 | ============================================================================== 40 | Writing debug statements *debuglog-write* 41 | 42 | Note that all debug loggers must be created using the dlog shim module: 43 | 44 | > 45 | -- Enable logging by running ":DebugLogEnable *" command first. 46 | 47 | local dlog = require("dlog") 48 | local logger1 = dlog.logger("logger1") 49 | local logger2 = dlog.logger("logger2") 50 | 51 | logger1("This is from %s", "logger1") 52 | logger1("This is also from %s", "logger1") 53 | logger2("And this is from %s", "logger2") 54 | 55 | -- you can also check if the logger is enabled if the value to print 56 | -- is expensive to get 57 | if dlog.is_enabled("logger1") then 58 | logger1("Print some heavy string: %s", "heavy string") 59 | end 60 | < 61 | 62 | You can create many named loggers, the logger name will be attached to all its 63 | messages. Additionally, log statements in the vim `:messages` will be nicely 64 | colored for easier identification: 65 | 66 | The loggers use standard Lua's string.format(). 67 | 68 | ============================================================================== 69 | User commands *debuglog-commands* 70 | 71 | - :DebugLogInstallShim 72 | Installs the |debuglog-shim| module into the specified directory. 73 | 74 | - :DebugLogEnable 75 | Enables the specified loggers. logspec could be * to enable all loggers, or 76 | a comma-separated list of logger names, e.g. logger1,logger2 77 | 78 | - :DebugLogDisable 79 | Disables all loggers. 80 | 81 | - :DebugLogEnableFileLogging 82 | Enables logging to a file. The file path will be printed out. 83 | 84 | - :DebugLogDisableFileLogging 85 | Disables logging to a file. 86 | 87 | - :DebugLogOpenFileLog 88 | Opens the debug log file in the current split. 89 | 90 | ============================================================================== 91 | Lua API *debuglog-lua* 92 | 93 | debuglog provides the following Lua API: 94 | 95 | > 96 | local debuglog = require("debuglog") 97 | 98 | -- Sets up the plugin, with default options. 99 | debuglog.setup({ 100 | log_to_console = true, 101 | log_to_file = false, 102 | -- The highlight group for printing the time column in console 103 | time_hl_group = "Comment", 104 | }) 105 | 106 | -- The same options as in the setup() function can be changed with set_config(). 107 | debuglog.set_config(opts) 108 | 109 | -- Enables all loggers. 110 | debuglog.enable("*") 111 | 112 | -- Enables specified loggers. 113 | debuglog.enable("logger1, logger2") 114 | 115 | -- Disable all loggers. 116 | debuglog.disable() 117 | 118 | -- Returns the file path with debug logs. 119 | debuglog.log_file_path() 120 | < 121 | -------------------------------------------------------------------------------- /lua/debuglog.lua: -------------------------------------------------------------------------------- 1 | -- Map of loggers by name. 2 | local loggers = {} 3 | -- Map of active loggers' options by name. 4 | local loggers_opts = {} 5 | local global_opts = { 6 | log_to_console = true, 7 | log_to_file = false, 8 | time_hl_group = "Comment" 9 | } 10 | -- Set of currently enabled loggers. 11 | local enabled_loggers = {} 12 | -- Means that * was specifed in the enabled logs spec. 13 | local all_enabled = false 14 | -- The log file path for the file logger. 15 | local outfile 16 | 17 | local colors = { 18 | "#0066CC", "#0099CC", "#0099FF", "#00CC00", "#00CC66", "#00CC99", "#00CCFF", 19 | "#3333FF", "#3366FF", "#3399FF", "#33CC00", "#33CC33", "#33CC66", "#33CC99", 20 | "#33CCCC", "#33CCFF", "#6600CC", "#6600FF", "#6633CC", "#6633FF", "#66CC00", 21 | "#66CC33", "#9900CC", "#9900FF", "#9933CC", "#9933FF", "#99CC00", "#99CC33", 22 | "#CC0000", "#CC0033", "#CC0066", "#CC0099", "#CC00CC", "#CC00FF", "#CC3300", 23 | "#CC3333", "#CC3366", "#CC3399", "#CC33CC", "#CC33FF", "#CC6600", "#CC6633", 24 | "#CC9900", "#CC9933", "#CCCC00", "#CCCC33", "#FF0000", "#FF0033", "#FF0066", 25 | "#FF0099", "#FF00CC", "#FF00FF", "#FF3300", "#FF3333", "#FF3366", "#FF3399", 26 | "#FF33CC", "#FF33FF", "#FF6600", "#FF6633", "#FF9900", "#FF9933", "#FFCC00", 27 | "#FFCC33" 28 | } 29 | -- Set of auto-generated highlight groups for colored log output. 30 | local hl_groups = {} 31 | 32 | ---Any simple hash function to make colors stick to loggers. 33 | local function simple_hash(s) 34 | local hash = 5381 35 | for i = 1, #s do 36 | hash = (bit.lshift(hash, 5) + hash) + string.byte(s, i) 37 | end 38 | return hash 39 | end 40 | 41 | local function make_logger(name, hl, opts) 42 | return function(...) 43 | if not opts.enabled then 44 | return 45 | end 46 | if global_opts.log_to_console then 47 | local message = string.format(...) 48 | vim.api.nvim_echo({ 49 | { os.date("%H:%M:%S:"), global_opts.time_hl_group }, { " " }, { name, hl }, 50 | { ": " }, { message } 51 | }, true, {}) 52 | end 53 | if global_opts.log_to_file then 54 | local fp = io.open(outfile, "a") 55 | local str = os.date("%H:%M:%S: ") .. string.format(...) .. "\n" 56 | fp:write(str) 57 | fp:close() 58 | end 59 | end 60 | end 61 | 62 | local function is_win() 63 | return package.config:sub(1, 1) == "\\" 64 | end 65 | 66 | local function path_sep() 67 | if is_win() then 68 | return "\\" 69 | end 70 | return "/" 71 | end 72 | 73 | local function path_escape(s) 74 | -- First, expand things like ~ since it does not always work in quotes 75 | s = vim.fn.expand(s) 76 | -- Then, normalize the path to remove .., //, etc... 77 | s = vim.fn.resolve(s) 78 | -- Finally, quote 79 | return "\"" .. s .. "\"" 80 | end 81 | 82 | local function script_path() 83 | local str = debug.getinfo(2, "S").source:sub(2) 84 | if is_win() then 85 | str = str:gsub("/", "\\") 86 | end 87 | return str:match("(.*" .. path_sep() .. ")") 88 | end 89 | 90 | local M = {} 91 | 92 | ---@class Options debug-log configuration options 93 | ---@field log_to_console boolean specifies whether to log messages to console, defaults to true 94 | ---@field log_to_file boolean specifies whether to log messages to debug file, defaults to false 95 | ---@field time_hl_group string the highlight group to use for message time, defaults to Comment 96 | local Options = {} 97 | 98 | ---Configures the plugin, registers user commands, etc. 99 | ---@param opts Options configuration options, optional 100 | function M.setup(opts) 101 | vim.cmd( 102 | [[comm! -nargs=1 DebugLogInstallShim :lua require("debuglog").install_shim()]]) 103 | vim.cmd( 104 | [[comm! -nargs=1 DebugLogEnable :lua require("debuglog").enable()]]) 105 | vim.cmd([[comm! DebugLogDisable :lua require("debuglog").disable()]]) 106 | vim.cmd( 107 | [[comm! DebugLogEnableFileLogging :lua require("debuglog").set_config({log_to_file = true})]]) 108 | vim.cmd( 109 | [[comm! DebugLogDisableFileLogging :lua require("debuglog").set_config({log_to_file = false})]]) 110 | vim.cmd( 111 | [[comm! DebugLogOpenFileLog :lua vim.cmd("e ".. require("debuglog").log_file_path())]]) 112 | outfile = string.format("%s/debug.log", 113 | vim.api.nvim_call_function("stdpath", { "data" })) 114 | M.set_config(opts) 115 | end 116 | 117 | ---Updates plugin configuration, for example to change the logging destinations. 118 | ---@param opts Options configurationt options 119 | function M.set_config(opts) 120 | opts = opts or {} 121 | for k, v in pairs(opts) do 122 | global_opts[k] = v 123 | end 124 | if opts.log_to_file then 125 | vim.notify("Logging to file " .. outfile) 126 | end 127 | end 128 | 129 | ---Returns the path to the debug log file. 130 | ---@return string log file path 131 | function M.log_file_path() 132 | return outfile 133 | end 134 | 135 | ---Enables the specified loggers. Note that previously enabled loggers will be disabled. 136 | ---@param spec string the log specification, comma-separated list of loggers to enable, or * to enable all loggers 137 | function M.enable(spec) 138 | spec = spec or "" 139 | M.disable() 140 | local split = vim.split(spec, ",") 141 | for _, l in ipairs(split) do 142 | if l == "*" then 143 | all_enabled = true 144 | for _, opts in pairs(loggers_opts) do 145 | opts.enabled = true 146 | end 147 | else 148 | enabled_loggers[l] = true 149 | local lopts = loggers_opts[l] 150 | if lopts then 151 | lopts.enabled = true 152 | end 153 | end 154 | end 155 | end 156 | 157 | ---Disables all loggers 158 | function M.disable() 159 | all_enabled = false 160 | for _, opts in pairs(loggers_opts) do 161 | opts.enabled = false 162 | end 163 | enabled_loggers = {} 164 | end 165 | 166 | ---Installs the dlog.lua shim to the specified directory 167 | ---@param dir string the destination directory 168 | function M.install_shim(dir) 169 | assert(dir and dir ~= "", "dir must be specified") 170 | 171 | local current_path = script_path() 172 | local current_dir = current_path:gsub(path_sep() .. "([^" .. path_sep() .. 173 | "]+)$", function() 174 | return "" 175 | end) 176 | local shim_path = current_dir .. "/../dlog.lua" 177 | local cmd 178 | local dest 179 | if is_win() then 180 | dest = path_escape(dir .. "\\dlog.lua") 181 | cmd = "copy /y " .. path_escape(shim_path) .. " " .. dest 182 | else 183 | dest = path_escape(dir .. "/dlog.lua") 184 | cmd = "cp -f " .. path_escape(shim_path) .. " " .. dest 185 | end 186 | if os.execute(cmd) ~= 0 then 187 | error("Could not copy the shim. Command used: " .. cmd) 188 | end 189 | vim.notify("Shim installed in " .. dest) 190 | end 191 | 192 | ---Do not use directly, this function should be called only from the shim 193 | function M.logger_for_shim_only(name) 194 | local logger = loggers[name] 195 | if logger then 196 | return logger 197 | end 198 | local opts = { enabled = all_enabled or enabled_loggers[name] } 199 | local hash = simple_hash(name) 200 | local color_index = (math.abs(hash) % #colors) + 1 201 | local hl = "DebugLog" .. color_index 202 | if not hl_groups[hl] then 203 | vim.cmd("hi! " .. hl .. " guifg=" .. colors[color_index]) 204 | hl_groups[hl] = true 205 | end 206 | logger = make_logger(name, hl, opts) 207 | loggers[name] = logger 208 | loggers_opts[name] = opts 209 | return logger 210 | end 211 | 212 | ---Checks if the logger is enabled. 213 | ---@param logger_name string the name of the logger 214 | ---@return boolean enabled whether the logger is enabled 215 | function M.is_enabled(logger_name) 216 | return enabled_loggers[logger_name] or false 217 | end 218 | 219 | return M 220 | --------------------------------------------------------------------------------