summaryrefslogtreecommitdiffstats
path: root/lua/components.lua
blob: 6298d3517c92e52dcc1e3997f337b4c1a9e24762 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
if not pcall(require, "el") then
  return
end

local Job = require "plenary.job"
local el_sub = require "el.subscribe"

local M = {}

function M.extract_hl(spec)
  if not spec or vim.tbl_isempty(spec) then return end
  local hl_name, hl_opts = { "El" }, {}
  for attr, val in pairs(spec) do
    if type(val) == "table" then
      table.insert(hl_name, attr)
      assert(vim.tbl_count(val) == 1)
      local hl, what = next(val)
      local hlID = vim.fn.hlID(hl)
      if hlID > 0 then
        table.insert(hl_name, hl)
        local col = vim.fn.synIDattr(hlID, what)
        if col and #col > 0 then
          table.insert(hl_name, what)
          hl_opts[attr] = col
        end
      end
    else
      -- bold, underline, etc
      hl_opts[attr] = val
    end
  end
  hl_name = table.concat(hl_name, "_")
  -- if highlight exists, verify it has
  -- the correct colorscheme highlights
  local newID = vim.fn.hlID(hl_name)
  if newID > 0 then
    for what, expected in pairs(hl_opts) do
      local res = vim.fn.synIDattr(newID, what)
      if type(expected) == "boolean" then
        -- synIDattr returns '1' for boolean
        res = res and res == "1" and true
      end
      if res ~= expected then
        -- need to regen the highlight
        -- print("color mismatch", hl_name, what, "e:", expected, "c:", res)
        newID = 0
      end
    end
  end
  if newID == 0 then
    vim.api.nvim_set_hl(0, hl_name, hl_opts)
  end
  return hl_name
end

local function set_hl(hls, s)
  if not hls or not s then return s end
  hls = type(hls) == "string" and { hls } or hls
  for _, hl in ipairs(hls) do
    if vim.fn.hlID(hl) > 0 then
      return ("%%#%s#%s%%0*"):format(hl, s)
    end
  end
  return s
end

local function wrap_fnc(opts, fn)
  return function(window, buffer)
    -- buf_autocmd doesn't send win
    if not window and buffer then
      window = { win_id = vim.fn.bufwinid(buffer.bufnr) }
    end
    if opts.hide_inactive and window and
        window.win_id ~= vim.api.nvim_get_current_win() then
      return ""
    end
    return fn(window, buffer)
  end
end

M.mode = function(opts)
  opts = opts or {}
  return wrap_fnc(opts, function(_, _)
    local fmt = opts.fmt or "%s%s"
    local mode = vim.api.nvim_get_mode().mode
    local mode_data = opts.modes and opts.modes[mode]
    local hls = mode_data and mode_data[3]
    local icon = opts.hl_icon_only and set_hl(hls, opts.icon) or opts.icon
    mode = mode_data and mode_data[1]:upper() or mode
    mode = (fmt):format(icon or "", mode)
    return not opts.hl_icon_only and set_hl(hls, mode) or mode
  end)
end

M.try_devicons = function()
  if not M._has_devicons then
    M._has_devicons, M._devicons = pcall(require, "nvim-web-devicons")
  end
  return M._devicons
end

M.file_icon = function(opts)
  opts = opts or {}
  return el_sub.buf_autocmd("el_file_icon", "BufRead",
    wrap_fnc(opts, function(_, buffer)
      if not M.try_devicons() then return "" end
      local fmt = opts.fmt or "%s"
      local ext = vim.fn.fnamemodify(buffer.name, ":p:e")
      local icon, hl = M._devicons.get_icon(buffer.name, ext:lower(), { default = true })
      -- local icon = extensions.file_icon(_, bufnr)
      if icon then
        if opts.hl_icon then
          local hlgroup = M.extract_hl({
            bg = { StatusLine = "bg" },
            fg = { [hl] = "fg" },
          })
          icon = set_hl(hlgroup, icon)
        end
        return (fmt):format(icon)
      end
      return ""
    end))
end

M.git_branch = function(opts)
  opts = opts or {}
  return el_sub.buf_autocmd("el_git_branch", "BufEnter",
    wrap_fnc(opts, function(_, buffer)
      -- Try fugitive first as it's most reliable
      local branch = vim.g.loaded_fugitive == 1 and
          vim.fn.FugitiveHead() or nil
      -- buffer can be null and code will crash with:
      -- E5108: Error executing lua ... 'attempt to index a nil value'
      if not buffer or not (buffer.bufnr > 0) then
        return
      end
      -- fugitive is empty or not loaded, try gitsigns
      if not branch or #branch == 0 then
        local ok, res = pcall(vim.api.nvim_buf_get_var,
          buffer.bufnr, "gitsigns_head")
        if ok then branch = res end
      end
      -- last resort run git command
      if not branch then
        local j = Job:new {
          command = "git",
          args = { "branch", "--show-current" },
          cwd = vim.fn.fnamemodify(buffer.name, ":h"),
        }

        local ok, result = pcall(function()
          return vim.trim(j:sync()[1])
        end)
        if ok then
          branch = result
        end
      end

      if branch and #branch > 0 then
        local fmt = opts.fmt or "%s %s"
        local icon = opts.icon or ""
        return set_hl(opts.hl, (fmt):format(icon, branch))
      end
    end))
end

local git_changes_formatter = function(opts)
  local specs = {
    insert = {
      regex = "(%d+) insertions?",
      icon  = opts.icon_insert or "+",
      hl    = opts.hl_insert,
    },
    change = {
      regex = "(%d+) files? changed",
      icon  = opts.icon_change or "~",
      hl    = opts.hl_change,
    },
    delete = {
      regex = "(%d+) deletions?",
      icon  = opts.icon_delete or "-",
      hl    = opts.hl_delete,
    },
  }
  return function(_, _, s)
    local result = {}
    for k, v in pairs(specs) do
      local count = nil
      if type(s) == "string" then
        -- 'git diff --shortstat' output
        -- from 'git_changes_all'
        count = tonumber(string.match(s, v.regex))
      else
        -- map from 'git_changes_buf'
        count = s[k]
      end
      if count and count > 0 then
        table.insert(result, set_hl(v.hl, ("%s%d"):format(v.icon, count)))
      end
    end
    return table.concat(result, ", ")
  end
end

-- requires gitsigns
M.git_changes_buf = function(opts)
  opts = opts or {}
  local formatter = opts.formatter or git_changes_formatter(opts)
  return wrap_fnc(opts, function(window, buffer)
    local stats = {}
    if buffer and buffer.bufnr > 0 then
      local ok, res = pcall(vim.api.nvim_buf_get_var,
        buffer.bufnr, "vgit_status")
      if ok then stats = res end
    end
    if buffer and buffer.bufnr > 0 then
      local ok, res = pcall(vim.api.nvim_buf_get_var,
        buffer.bufnr, "gitsigns_status_dict")
      if ok then stats = res end
    end
    local counts = {
      insert = stats.added > 0 and stats.added or nil,
      change = stats.changed > 0 and stats.changed or nil,
      delete = stats.removed > 0 and stats.removed or nil,
    }
    if not vim.tbl_isempty(counts) then
      local fmt = opts.fmt or "%s"
      local out = formatter(window, buffer, counts)
      return out and fmt:format(out) or nil
    else
      -- el functions that return a
      -- string must not return nil
      return ""
    end
  end)
end

M.git_changes_all = function(opts)
  opts = opts or {}
  local formatter = opts.formatter or git_changes_formatter(opts)
  return el_sub.buf_autocmd("el_git_changes", "BufWritePost",
    wrap_fnc(opts, function(window, buffer)
      if not buffer or
          not (buffer.bufnr > 0) or
          vim.bo[buffer.bufnr].bufhidden ~= "" or
          vim.bo[buffer.bufnr].buftype == "nofile" or
          vim.fn.filereadable(buffer.name) ~= 1 then
        return
      end

      local j = Job:new {
        command = "git",
        args = { "diff", "--shortstat" },
        -- makes no sense to run for one file as
        -- 'file(s) changed' will always be 1
        -- args = { "diff", "--shortstat", buffer.name },
        cwd = vim.fn.fnamemodify(buffer.name, ":h"),
      }

      local ok, git_changes = pcall(function()
        return formatter(window, buffer, vim.trim(j:sync()[1]))
      end)

      if ok then
        local fmt = opts.fmt or "%s"
        return git_changes and fmt:format(git_changes) or nil
      end
    end))
end

local function lsp_srvname(bufnr)
  local buf_clients = vim.lsp.buf_get_clients(bufnr)
  if not buf_clients or #buf_clients == 0 then
    return nil
  end
  local names = ""
  for i, c in ipairs(buf_clients) do
    if i > 1 then names = names .. ", " end
    names = names .. c.name
  end
  return names
end

local function diag_formatter(opts)
  return function(_, buffer, counts)
    local items = {}
    local icons = {
      ["errors"]   = { opts.icon_err or "E", opts.hl_err },
      ["warnings"] = { opts.icon_warn or "W", opts.hl_warn },
      ["infos"]    = { opts.icon_info or "I", opts.hl_info },
      ["hints"]    = { opts.icon_hint or "H", opts.hl_hint },
    }
    for _, k in ipairs({ "errors", "warnings", "infos", "hints" }) do
      if counts[k] > 0 then
        table.insert(items,
          set_hl(icons[k][2], ("%s:%s"):format(icons[k][1], counts[k])))
      end
    end
    local fmt = opts.fmt or "%s"
    local lsp_name = opts.lsp and lsp_srvname(buffer.bufnr)
    if not lsp_name and vim.tbl_isempty(items) then
      return ""
    else
      local contents = lsp_name
      if not vim.tbl_isempty(items) then
        contents = ("%s %s"):format(lsp_name, table.concat(items, " "))
      end
      return fmt:format(contents)
    end
  end
end

local get_buffer_counts = function(diagnostic, _, buffer)
  local counts = { 0, 0, 0, 0 }
  local diags = diagnostic.get(buffer.bufnr)
  if diags and not vim.tbl_isempty(diags) then
    for _, d in ipairs(diags) do
      if tonumber(d.severity) then
        counts[d.severity] = counts[d.severity] + 1
      end
    end
  end
  return {
    errors   = counts[1],
    warnings = counts[2],
    infos    = counts[3],
    hints    = counts[4],
  }
end

M.diagnostics = function(opts)
  opts = opts or {}
  local formatter = opts.formatter or diag_formatter(opts)
  return el_sub.buf_autocmd("el_buf_diagnostic", "LspAttach,DiagnosticChanged",
    wrap_fnc(opts, function(window, buffer)
      return formatter(window, buffer, get_buffer_counts(vim.diagnostic, window, buffer))
    end))
end

return M