1-- Logger for language client plugin.
2
3local log = {}
4
5-- FIXME: DOC
6-- Should be exposed in the vim docs.
7--
8-- Log level dictionary with reverse lookup as well.
9--
10-- Can be used to lookup the number from the name or the name from the number.
11-- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR"
12-- Level numbers begin with "TRACE" at 0
13log.levels = vim.deepcopy(vim.log.levels)
14
15-- Default log level is warn.
16local current_log_level = log.levels.WARN
17local log_date_format = "%F %H:%M:%S"
18local format_func = function(arg) return vim.inspect(arg, {newline=''}) end
19
20do
21  local path_sep = vim.loop.os_uname().version:match("Windows") and "\\" or "/"
22  ---@private
23  local function path_join(...)
24    return table.concat(vim.tbl_flatten{...}, path_sep)
25  end
26  local logfilename = path_join(vim.fn.stdpath('cache'), 'lsp.log')
27
28  --- Returns the log filename.
29  ---@returns (string) log filename
30  function log.get_filename()
31    return logfilename
32  end
33
34  vim.fn.mkdir(vim.fn.stdpath('cache'), "p")
35  local logfile = assert(io.open(logfilename, "a+"))
36
37  local log_info = vim.loop.fs_stat(logfilename)
38  if log_info and log_info.size > 1e9 then
39    local warn_msg = string.format(
40      "LSP client log is large (%d MB): %s",
41      log_info.size / (1000 * 1000),
42      logfilename
43    )
44    vim.notify(warn_msg)
45  end
46
47  -- Start message for logging
48  logfile:write(string.format("[START][%s] LSP logging initiated\n", os.date(log_date_format)))
49  for level, levelnr in pairs(log.levels) do
50    -- Also export the log level on the root object.
51    log[level] = levelnr
52    -- FIXME: DOC
53    -- Should be exposed in the vim docs.
54    --
55    -- Set the lowercase name as the main use function.
56    -- If called without arguments, it will check whether the log level is
57    -- greater than or equal to this one. When called with arguments, it will
58    -- log at that level (if applicable, it is checked either way).
59    --
60    -- Recommended usage:
61    -- ```
62    -- local _ = log.warn() and log.warn("123")
63    -- ```
64    --
65    -- This way you can avoid string allocations if the log level isn't high enough.
66    log[level:lower()] = function(...)
67      local argc = select("#", ...)
68      if levelnr < current_log_level then return false end
69      if argc == 0 then return true end
70      local info = debug.getinfo(2, "Sl")
71      local header = string.format("[%s][%s] ...%s:%s", level, os.date(log_date_format), string.sub(info.short_src, #info.short_src - 15), info.currentline)
72      local parts = { header }
73      for i = 1, argc do
74        local arg = select(i, ...)
75        if arg == nil then
76          table.insert(parts, "nil")
77        else
78          table.insert(parts, format_func(arg))
79        end
80      end
81      logfile:write(table.concat(parts, '\t'), "\n")
82      logfile:flush()
83    end
84  end
85end
86
87-- This is put here on purpose after the loop above so that it doesn't
88-- interfere with iterating the levels
89vim.tbl_add_reverse_lookup(log.levels)
90
91--- Sets the current log level.
92---@param level (string or number) One of `vim.lsp.log.levels`
93function log.set_level(level)
94  if type(level) == 'string' then
95    current_log_level = assert(log.levels[level:upper()], string.format("Invalid log level: %q", level))
96  else
97    assert(type(level) == 'number', "level must be a number or string")
98    assert(log.levels[level], string.format("Invalid log level: %d", level))
99    current_log_level = level
100  end
101end
102
103--- Gets the current log level.
104---@return string current log level
105function log.get_level()
106  return current_log_level
107end
108
109--- Sets formatting function used to format logs
110---@param handle function function to apply to logging arguments, pass vim.inspect for multi-line formatting
111function log.set_format_func(handle)
112  assert(handle == vim.inspect or type(handle) == 'function', "handle must be a function")
113  format_func = handle
114end
115
116--- Checks whether the level is sufficient for logging.
117---@param level number log level
118---@returns (bool) true if would log, false if not
119function log.should_log(level)
120  return level >= current_log_level
121end
122
123return log
124-- vim:sw=2 ts=2 et
125