nvim/after/plugin/statusline.lua
2023-09-23 23:29:54 +02:00

764 lines
24 KiB
Lua
Executable file

local conditions = require("heirline.conditions")
local utils = require("heirline.utils")
local colors = {
bright_bg = utils.get_highlight("Folded").bg,
bright_fg = utils.get_highlight("Folded").fg,
dark_bg = "#0c0c0c",
red = utils.get_highlight("DiagnosticError").fg,
dark_red = utils.get_highlight("DiffDelete").bg,
green = utils.get_highlight("String").fg,
blue = utils.get_highlight("Function").fg,
gray = utils.get_highlight("NonText").fg,
orange = utils.get_highlight("Constant").fg,
purple = utils.get_highlight("Statement").fg,
cyan = utils.get_highlight("Special").fg,
diag_warn = utils.get_highlight("DiagnosticWarn").fg,
diag_error = utils.get_highlight("DiagnosticError").fg,
diag_hint = utils.get_highlight("DiagnosticHint").fg,
diag_info = utils.get_highlight("DiagnosticInfo").fg,
git_del = utils.get_highlight("DiffRemoved").fg,
git_add = utils.get_highlight("DiffAdd").fg,
git_change = utils.get_highlight("DiffChanged").fg,
}
require("heirline").load_colors(colors)
local ViMode = {
-- get vim current mode, this information will be required by the provider
-- and the highlight functions, so we compute it only once per component
-- evaluation and store it as a component attribute
init = function(self)
self.mode = vim.fn.mode(1) -- :h mode()
end,
-- Now we define some dictionaries to map the output of mode() to the
-- corresponding string and color. We can put these into `static` to compute
-- them at initialisation time.
static = {
mode_names = { -- change the strings if you like it vvvvverbose!
n = "N",
no = "N?",
nov = "N?",
noV = "N?",
["no\22"] = "N?",
niI = "Ni",
niR = "Nr",
niV = "Nv",
nt = "Nt",
v = "V",
vs = "Vs",
V = "V_",
Vs = "Vs",
["\22"] = "^V",
["\22s"] = "^V",
s = "S",
S = "S_",
["\19"] = "^S",
i = "I",
ic = "Ic",
ix = "Ix",
R = "R",
Rc = "Rc",
Rx = "Rx",
Rv = "Rv",
Rvc = "Rv",
Rvx = "Rv",
c = "C",
cv = "Ex",
r = "...",
rm = "M",
["r?"] = "?",
["!"] = "!",
t = "T",
},
mode_colors = {
n = "red" ,
i = "green",
v = "cyan",
V = "cyan",
["\22"] = "cyan",
c = "orange",
s = "purple",
S = "purple",
["\19"] = "purple",
R = "orange",
r = "orange",
["!"] = "red",
t = "red",
}
},
-- We can now access the value of mode() that, by now, would have been
-- computed by `init()` and use it to index our strings dictionary.
-- note how `static` fields become just regular attributes once the
-- component is instantiated.
-- To be extra meticulous, we can also add some vim statusline syntax to
-- control the padding and make sure our string is always at least 2
-- characters long. Plus a nice Icon.
provider = function(self)
--return "  %2("..self.mode_names[self.mode].."%)"
return "%2("..self.mode_names[self.mode].."%)"
end,
-- Same goes for the highlight. Now the foreground will change according to the current mode.
hl = function(self)
local mode = self.mode:sub(1, 1) -- get only the first mode character
return { fg = self.mode_colors[mode], bold = true, }
end,
-- Re-evaluate the component only on ModeChanged event!
-- Also allorws the statusline to be re-evaluated when entering operator-pending mode
update = {
"ModeChanged",
pattern = "*:*",
callback = vim.schedule_wrap(function()
vim.cmd("redrawstatus")
end),
},
}
ViMode = utils.surround({ "", "" }, "dark_bg", { ViMode})
local FileNameBlock = {
-- let's first set up some attributes needed by this component and it's children
init = function(self)
self.filename = vim.api.nvim_buf_get_name(0)
end,
}
-- We can now define some children separately and add them later
local FileIcon = {
init = function(self)
local filename = self.filename
local extension = vim.fn.fnamemodify(filename, ":e")
self.icon, self.icon_color = require("nvim-web-devicons").get_icon_color(filename, extension, { default = true })
end,
provider = function(self)
return self.icon and (self.icon .. " ")
end,
hl = function(self)
return { fg = self.icon_color }
end
}
local FileName = {
provider = function(self)
-- first, trim the pattern relative to the current directory. For other
-- options, see :h filename-modifers
local filename = vim.fn.fnamemodify(self.filename, ":.")
if filename == "" then return "[No Name]" end
-- now, if the filename would occupy more than 1/4th of the available
-- space, we trim the file path to its initials
-- See Flexible Components section below for dynamic truncation
if not conditions.width_percent_below(#filename, 0.25) then
filename = vim.fn.pathshorten(filename)
end
return filename
end,
hl = { fg = utils.get_highlight("Directory").fg },
}
local FileFlags = {
{
condition = function()
return vim.bo.modified
end,
provider = "[+]",
hl = { fg = "green" },
},
{
condition = function()
return not vim.bo.modifiable or vim.bo.readonly
end,
provider = "",
hl = { fg = "orange" },
},
}
-- Now, let's say that we want the filename color to change if the buffer is
-- modified. Of course, we could do that directly using the FileName.hl field,
-- but we'll see how easy it is to alter existing components using a "modifier"
-- component
local FileNameModifer = {
hl = function()
if vim.bo.modified then
-- use `force` because we need to override the child's hl foreground
return { fg = "cyan", bold = true, force=true }
end
end,
}
-- let's add the children to our FileNameBlock component
FileNameBlock = utils.insert(FileNameBlock,
FileIcon,
utils.insert(FileNameModifer, FileName), -- a new table where FileName is a child of FileNameModifier
FileFlags,
{ provider = '%<'} -- this means that the statusline is cut here when there's not enough space
)
local FileSize = {
provider = function()
-- stackoverflow, compute human readable file size
local suffix = { 'b', 'k', 'M', 'G', 'T', 'P', 'E' }
local fsize = vim.fn.getfsize(vim.api.nvim_buf_get_name(0))
fsize = (fsize < 0 and 0) or fsize
if fsize < 1024 then
return fsize..suffix[1]
end
local i = math.floor((math.log(fsize) / math.log(1024)))
return string.format("%.2g%s", fsize / math.pow(1024, i), suffix[i + 1])
end
}
local FileLastModified = {
-- did you know? Vim is full of functions!
provider = function()
local ftime = vim.fn.getftime(vim.api.nvim_buf_get_name(0))
return (ftime > 0) and os.date("%c", ftime)
end
}
FileNameBlock = utils.surround({ "", "" }, "dark_bg", FileNameBlock)
-- We're getting minimalists here!
local Ruler = {
-- %l = current line number
-- %L = number of lines in the buffer
-- %c = column number
-- %P = percentage through file of displayed window
provider = "%7(%l/%3L%):%2c %P",
}
Ruler = utils.surround({ "", "" }, "dark_bg", Ruler)
-- I take no credits for this! :lion:
local ScrollBar ={
static = {
sbar = { '', '', '', '', '', '', '', '' }
-- Another variant, because the more choice the better.
-- sbar = { '🭶', '🭷', '🭸', '🭹', '🭺', '🭻' }
},
provider = function(self)
local curr_line = vim.api.nvim_win_get_cursor(0)[1]
local lines = vim.api.nvim_buf_line_count(0)
local i = math.floor((curr_line - 1) / lines * #self.sbar) + 1
return string.rep(self.sbar[i], 2)
end,
hl = { fg = "blue", bg = "dark_bg" },
}
local LSPActive = {
condition = conditions.lsp_attached,
update = {'LspAttach', 'LspDetach'},
-- You can keep it simple,
-- provider = " [LSP]",
-- Or complicate things a bit and get the servers names
provider = function()
local names = {}
for i, server in pairs(vim.lsp.get_active_clients({ bufnr = 0 })) do
table.insert(names, server.name)
end
return " [" .. table.concat(names, " ") .. "]"
end,
hl = { fg = "green", bold = true },
}
LSPActive = utils.surround({ "", "" }, "dark_bg", LSPActive)
-- Full nerd (with icon colors and clickable elements)!
-- works in multi window, but does not support flexible components (yet ...)
local Navic = {
condition = function() return require("nvim-navic").is_available() end,
static = {
-- create a type highlight map
type_hl = {
File = "Directory",
Module = "@include",
Namespace = "@namespace",
Package = "@include",
Class = "@structure",
Method = "@method",
Property = "@property",
Field = "@field",
Constructor = "@constructor",
Enum = "@field",
Interface = "@type",
Function = "@function",
Variable = "@variable",
Constant = "@constant",
String = "@string",
Number = "@number",
Boolean = "@boolean",
Array = "@field",
Object = "@type",
Key = "@keyword",
Null = "@comment",
EnumMember = "@field",
Struct = "@structure",
Event = "@keyword",
Operator = "@operator",
TypeParameter = "@type",
},
-- bit operation dark magic, see below...
enc = function(line, col, winnr)
return bit.bor(bit.lshift(line, 16), bit.lshift(col, 6), winnr)
end,
-- line: 16 bit (65535); col: 10 bit (1023); winnr: 6 bit (63)
dec = function(c)
local line = bit.rshift(c, 16)
local col = bit.band(bit.rshift(c, 6), 1023)
local winnr = bit.band(c, 63)
return line, col, winnr
end
},
init = function(self)
local data = require("nvim-navic").get_data() or {}
local children = {}
-- create a child for each level
for i, d in ipairs(data) do
-- encode line and column numbers into a single integer
local pos = self.enc(d.scope.start.line, d.scope.start.character, self.winnr)
local child = {
{
provider = d.icon,
hl = self.type_hl[d.type],
},
{
-- escape `%`s (elixir) and buggy default separators
provider = d.name:gsub("%%", "%%%%"):gsub("%s*->%s*", ''),
-- highlight icon only or location name as well
-- hl = self.type_hl[d.type],
on_click = {
-- pass the encoded position through minwid
minwid = pos,
callback = function(_, minwid)
-- decode
local line, col, winnr = self.dec(minwid)
vim.api.nvim_win_set_cursor(vim.fn.win_getid(winnr), {line, col})
end,
name = "heirline_navic",
},
},
}
-- add a separator only if needed
if #data > 1 and i < #data then
table.insert(child, {
provider = " > ",
hl = { fg = 'bright_fg' },
})
end
table.insert(children, child)
end
-- instantiate the new child, overwriting the previous one
self.child = self:new(children, 1)
end,
-- evaluate the children containing navic components
provider = function(self)
return self.child:eval()
end,
hl = { fg = "gray" },
update = 'CursorMoved'
}
-- local Diagnostics = {
--
-- condition = conditions.has_diagnostics,
--
-- static = {
-- error_icon = vim.fn.sign_getdefined("DiagnosticSignError")[1].text,
-- warn_icon = vim.fn.sign_getdefined("DiagnosticSignWarn")[1].text,
-- info_icon = vim.fn.sign_getdefined("DiagnosticSignInfo")[1].text,
-- hint_icon = vim.fn.sign_getdefined("DiagnosticSignHint")[1].text,
-- },
--
-- init = function(self)
-- self.errors = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.ERROR })
-- self.warnings = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.WARN })
-- self.hints = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.HINT })
-- self.info = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.INFO })
-- end,
--
-- update = { "DiagnosticChanged", "BufEnter" },
--
-- {
-- provider = function(self)
-- -- 0 is just another output, we can decide to print it or not!
-- return self.errors > 0 and (self.error_icon .. self.errors .. " ")
-- end,
-- hl = { fg = "diag_error" },
-- },
-- {
-- provider = function(self)
-- return self.warnings > 0 and (self.warn_icon .. self.warnings .. " ")
-- end,
-- hl = { fg = "diag_warn" },
-- },
-- {
-- provider = function(self)
-- return self.info > 0 and (self.info_icon .. self.info .. " ")
-- end,
-- hl = { fg = "diag_info" },
-- },
-- {
-- provider = function(self)
-- return self.hints > 0 and (self.hint_icon .. self.hints)
-- end,
-- hl = { fg = "diag_hint" },
-- },
-- }
local Git = {
condition = conditions.is_git_repo,
init = function(self)
self.status_dict = vim.b.gitsigns_status_dict
self.has_changes = self.status_dict.added ~= 0 or self.status_dict.removed ~= 0 or self.status_dict.changed ~= 0
end,
hl = { fg = "orange" },
{ -- git branch name
provider = function(self)
return "" .. self.status_dict.head
end,
hl = { bold = true }
},
-- You could handle delimiters, icons and counts similar to Diagnostics
{
condition = function(self)
return self.has_changes
end,
provider = "("
},
{
provider = function(self)
local count = self.status_dict.added or 0
return count > 0 and ("+" .. count)
end,
hl = { fg = "green" },
},
{
provider = function(self)
local count = self.status_dict.removed or 0
return count > 0 and ("-" .. count)
end,
hl = { fg = "red" },
},
{
provider = function(self)
local count = self.status_dict.changed or 0
return count > 0 and ("~" .. count)
end,
hl = { fg = "blue" },
},
{
condition = function(self)
return self.has_changes
end,
provider = ")",
},
}
local DAPMessages = {
condition = function()
local session = require("dap").session()
return session ~= nil
end,
provider = function()
return "" .. require("dap").status()
end,
hl = "Debug"
-- see Click-it! section for clickable actions
}
local WorkDir = {
provider = function()
local icon = (vim.fn.haslocaldir(0) == 1 and "l" or "g") .. " " .. ""
local cwd = vim.fn.getcwd(0)
cwd = vim.fn.fnamemodify(cwd, ":~")
if not conditions.width_percent_below(#cwd, 0.25) then
cwd = vim.fn.pathshorten(cwd)
end
local trail = cwd:sub(-1) == '/' and '' or "/"
return icon .. cwd .. trail
end,
hl = { fg = "blue", bold = true },
}
local TerminalName = {
-- we could add a condition to check that buftype == 'terminal'
-- or we could do that later (see #conditional-statuslines below)
provider = function()
local tname, _ = vim.api.nvim_buf_get_name(0):gsub(".*:", "")
return "" .. tname
end,
hl = { fg = "blue", bold = true },
}
-------------------------------------------------------------------------------------
local TablineBufnr = {
provider = function(self)
return tostring(self.bufnr) .. ". "
end,
hl = "Comment",
}
-- we redefine the filename component, as we probably only want the tail and not the relative path
local TablineFileName = {
provider = function(self)
-- self.filename will be defined later, just keep looking at the example!
local filename = self.filename
filename = filename == "" and "[No Name]" or vim.fn.fnamemodify(filename, ":t")
return filename
end,
hl = function(self)
return { bold = self.is_active or self.is_visible, italic = true }
end,
}
-- this looks exactly like the FileFlags component that we saw in
-- #crash-course-part-ii-filename-and-friends, but we are indexing the bufnr explicitly
-- also, we are adding a nice icon for terminal buffers.
local TablineFileFlags = {
{
condition = function(self)
return vim.api.nvim_buf_get_option(self.bufnr, "modified")
end,
provider = "[+]",
hl = { fg = "green" },
},
{
condition = function(self)
return not vim.api.nvim_buf_get_option(self.bufnr, "modifiable")
or vim.api.nvim_buf_get_option(self.bufnr, "readonly")
end,
provider = function(self)
if vim.api.nvim_buf_get_option(self.bufnr, "buftype") == "terminal" then
return ""
else
return ""
end
end,
hl = { fg = "orange" },
},
}
-- Here the filename block finally comes together
local TablineFileNameBlock = {
init = function(self)
self.filename = vim.api.nvim_buf_get_name(self.bufnr)
end,
hl = function(self)
if self.is_active then
return "TabLineSel"
-- why not?
-- elseif not vim.api.nvim_buf_is_loaded(self.bufnr) then
-- return { fg = "gray" }
else
return "TabLine"
end
end,
on_click = {
callback = function(_, minwid, _, button)
if (button == "m") then -- close on mouse middle click
vim.schedule(function()
vim.api.nvim_buf_delete(minwid, { force = false })
end)
else
vim.api.nvim_win_set_buf(0, minwid)
end
end,
minwid = function(self)
return self.bufnr
end,
name = "heirline_tabline_buffer_callback",
},
TablineBufnr,
FileIcon, -- turns out the version defined in #crash-course-part-ii-filename-and-friends can be reutilized as is here!
TablineFileName,
TablineFileFlags,
}
-- a nice "x" button to close the buffer
local TablineCloseButton = {
condition = function(self)
return not vim.api.nvim_buf_get_option(self.bufnr, "modified")
end,
{ provider = " " },
{
provider = "",
hl = { fg = "gray" },
on_click = {
callback = function(_, minwid)
vim.schedule(function()
vim.api.nvim_buf_delete(minwid, { force = false })
end)
vim.cmd.redrawtabline()
end,
minwid = function(self)
return self.bufnr
end,
name = "heirline_tabline_close_buffer_callback",
},
},
}
-- The final touch!
local TablineBufferBlock = utils.surround({ "", "" }, function(self)
if self.is_active then
return utils.get_highlight("TabLineSel").bg
else
return utils.get_highlight("TabLine").bg
end
end, { TablineFileNameBlock, TablineCloseButton })
-- and here we go
local BufferLine = utils.make_buflist(
TablineBufferBlock,
{ provider = "", hl = { fg = "gray" } }, -- left truncation, optional (defaults to "<")
{ provider = "", hl = { fg = "gray" } } -- right trunctation, also optional (defaults to ...... yep, ">")
-- by the way, open a lot of buffers and try clicking them ;)
)
local Tabpage = {
provider = function(self)
return "%" .. self.tabnr .. "T " .. self.tabpage .. " %T"
end,
hl = function(self)
if not self.is_active then
return "TabLine"
else
return "TabLineSel"
end
end,
}
local TabpageClose = {
provider = "%999X  %X",
hl = "TabLine",
}
local TabPages = {
-- only show this component if there's 2 or more tabpages
condition = function()
return #vim.api.nvim_list_tabpages() >= 2
end,
{ provider = "%=" },
utils.make_tablist(Tabpage),
TabpageClose,
}
local TabLineOffset = {
condition = function(self)
local win = vim.api.nvim_tabpage_list_wins(0)[1]
local bufnr = vim.api.nvim_win_get_buf(win)
self.winid = win
if vim.bo[bufnr].filetype == "NvimTree" then
self.title = "NvimTree"
return true
elseif vim.bo[bufnr].filetype == "neo-tree" then
self.title = "NeoTree"
return true
elseif vim.bo[bufnr].filetype == "nerdtree" then
self.title = "NerdTree"
return true
end
-- elseif vim.bo[bufnr].filetype == "TagBar" then
-- ...
end,
provider = function(self)
local title = self.title
local width = vim.api.nvim_win_get_width(self.winid)
local pad = math.ceil((width - #title) / 2)
return string.rep(" ", pad) .. title .. string.rep(" ", pad)
end,
hl = function(self)
if vim.api.nvim_get_current_win() == self.winid then
return "TablineSel"
else
return "Tabline"
end
end,
}
local Align = { provider = "%=" }
local Space = { provider = " " }
local StatusLine = {
ViMode, Space, FileNameBlock, Space, Git, Space, Align,
DAPMessages, Align,
LSPActive, Space, Ruler
}
local WinBar = {
utils.surround({"", ""}, "#000000", Navic)
}
local TabBar = {
TabLineOffset, BufferLine, TabPages,
}
require("heirline").setup({
statusline = StatusLine,
tabline = TabBar,
winbar = WinBar,
opts = {
disable_winbar_cb = function(args)
local buf = args.buf
local buftype = vim.tbl_contains({ "prompt", "nofile", "help", "quickfix" }, vim.bo[buf].buftype)
local filetype = vim.tbl_contains({ "gitcommit", "fugitive", "Trouble", "packer", "neo-tree", "nerdtree", "NvimTree" }, vim.bo[buf].filetype)
return buftype or filetype
end,
}
})
vim.o.showtabline = 2
vim.o.laststatus = 3
-- require("lualine").setup {
-- options = {
-- icons_enabled = true,
-- theme = "auto",
-- component_separators = { left = "", right = ""},
-- section_separators = { left = "", right = ""},
-- disabled_filetypes = {
-- statusline = {},
-- winbar = {},
-- },
-- ignore_focus = {},
-- always_divide_middle = true,
-- globalstatus = false,
-- refresh = {
-- statusline = 1000,
-- tabline = 1000,
-- winbar = 1000,
-- }
-- },
-- sections = {
-- lualine_a = {"mode"},
-- lualine_b = {"diff"},
-- lualine_c = {"branch"},
-- lualine_x = {"location"},
-- lualine_y = {"progress"},
-- lualine_z = {"filename"}
-- },
-- inactive_sections = {
-- lualine_a = {},
-- lualine_b = {},
-- lualine_c = {"filename"},
-- lualine_x = {"location"},
-- lualine_y = {},
-- lualine_z = {}
-- },
-- tabline = {},
-- winbar = {},
-- inactive_winbar = {},
-- extensions = {}
-- }