summaryrefslogtreecommitdiffstats
path: root/lua/components.lua
blob: cce3127e69c28498aa8fbe6e601bcc8e126a5ae2 (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
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

function M.mode(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

function M.git_branch(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 function git_changes_formatter(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
function M.git_changes_buf(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

function M.git_changes_all(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

function M.lsp_srvname(opts)
  local fmt = opts.fmt or "%s"
  local icon = opts.icon or ""
  local buf_clients = vim.lsp.buf_get_clients(0)
  if not buf_clients or #buf_clients == 0 then
    return ""
  end
  local names = ""
  for i, c in ipairs(buf_clients) do
    if i > 1 then names = names .. ", " end
    names = names .. c.name
  end
  return icon..fmt:format(names)
end

local function diag_formatter(opts)
  return function(_, buffer, counts)
    local items = {}
    local icons = {
      ["errors"]   = { opts.icon_err or "x", opts.hl_err or "DiagnosticError"},
      ["warnings"] = { opts.icon_warn or "!", opts.hl_warn or "DiagnosticWarn"},
      ["infos"]    = { opts.icon_info or "i", opts.hl_info or "DiagnosticInfo"},
      ["hints"]    = { opts.icon_hint or "h", opts.hl_hint or "DiagnosticHint"},
    }
    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"
    if vim.tbl_isempty(items) then
      return ""
    else
      return fmt:format(table.concat(items, " "))
    end
  end
end

local function get_buffer_counts(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

function M.diagnostics(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

function M.line(opts)
  opts = opts or {}
  local fmt = opts.fmt or "%s"
  return wrap_fnc(opts, function(_, _)
    local al = vim.api.nvim_buf_line_count(0)
    local cl = vim.api.nvim_win_get_cursor(0)[1]
    return (fmt):format(cl.."/"..al.." "..math.floor((cl / al) * 100).."%%")
  end)
end

return M