-
Notifications
You must be signed in to change notification settings - Fork 2
Guide_Statuscolumn
Important
As statuscolumn
functions mostly the same as statusline
, I will be skipping over the basics. So, check that out first if you are new.
I recommend you try reading :h 'statusline'
& :h 'statuscolumn'
first as it would help you understand how they work better.
The statuscolumn
works pretty much like the statusline
with a few changes.
You can also modify it's contents just like the statusline.
:set statuscolumn=Hi!
Note
Depending on your configuration you statuscolumn might look weird. So, you should set these options before setting the statuscolumn.
--- Use this so that the statuscolumn updates
--- when you move around.
vim.o.relativenumber = true;
--- This can cause the statuscolumn to look
--- wider than it actually is.
--- > On 0.10, this can cause Neovim to crash
--- > when using click handlers if a higher
--- > value is used.
vim.o.numberwidth = 1;
The statuscolumn is run on every line and thus there's no real advantage of using some text literally.
You will most likely use some statuscolumn items
instead.

Just like the statusline, the statuscolumn
also has some statuscolumn items.
You can get a functional statuscolumn quite easily using these items,
Important
Make sure you have number
enabled.
set number
:set statuscolumn=%s%C%3l\ ▍
Let's break it down,
Part | Description |
---|---|
%s |
The sign column. |
%C |
The fold column. |
%3l |
The line number. It has a width of 3 columns. |
\ |
Space. |
▍ |
Border to separate the statuscolumn from the text. |
Of course just like the statusline
, we can also add colors to the statuscolumn.
:set statuscolumn=%#Normal#%s%C%#Special#%3l%#Normal#\ ▍
I used
%#Normal#
before the statuscolumn as I generally prefer not to use a different color in the statuscolumn, so that is optional.
We can also have clickable sections in the statuscolumn. However, you must use the same function on every line of the statuscolumn.
And don't try to programmatically change it, that doesn't work.
So, we first need to create a function.
--- Goes to the clicked line number.
_G.__to_line = function ()
---@type table
local mousepos = vim.fn.getmousepos();
---@type [ integer, integer ]
local cursor = vim.api.nvim_win_get_cursor(mousepos.winid);
pcall(vim.api.nvim_win_set_cursor, mousepos.winid, { mousepos.line, cursor[2] });
end
As the same function is used for handling clicks for every line in the statuscolumn, we need a way to check which line was clicked.
We do this via mousepos()
which returns a table that has the line we clicked.
We also store the current cursor position so that it acts the same way as using
j
&k
. But this can sometimes causePos outside of buffer
error. So we wrap it inpcall()
to ignore errors.
Now we can use it as a click_handler,
:set statuscolumn=%#Normal#%s%C%#Special#%@v:lua.__to_line@%3l%X%#Normal#\ ▍
We can create a start a click region by surrounding the function name(we use v:lua
to access functions in lua) between %@
& @
.
We end the click region by using %X
(or %T
).

We set the statuscolumn just like the statusline.
Note
As with the statusline
, I will be assuming your statuscolumn file exists at ~/.config/nvim/lua/statuscolumn.lua
.
local statuscolumn = {};
--- Helper function for applying
--- highlight groups.
---@param hl string
---@return string
local function set_hl (hl)
if type(hl) ~= "string" then
return "";
elseif vim.fn.hlexists(hl) == 0 then
return "";
else
return "%#" .. hl .. "#";
end
end
--- Optional, configuration table.
--- Add this if you like tinkering.
statuscolumn.config = {};
--- Function to create the statuscolumn.
---@return string
statuscolumn.render = function ()
local _statuscolumn = "";
--- Window whose statuscolumn we are
--- creating.
--- No, this is not a typo.
---@type integer
local window = vim.g.statusline_winid;
--- Buffer of the window.
---@type integer
local buffer = vim.api.nvim_win_get_buf(window);
for _, component in ipairs(statuscolumn.config) do
local success, part_text = pcall(statuscolumn[component.kind], buffer, window, component);
if success then
--- Only add text if a function doesn't fail.
_statuscolumn = _statuscolumn .. part_text;
end
end
return _statuscolumn;
end
--- Optional, setup function.
statuscolumn.setup = function (config)
if type(config) == "table" then
statuscolumn.config = vim.tbl_deep_extend("force", statuscolumn.config, config);
end
vim.o.relativenumber = true;
vim.o.numberwidth = 1;
vim.o.statuscolumn = "%!v:lua.require('statuscolumn').render()";
end
return statuscolumn;
Now, we can use it in our init.lua
.
--- Change the path to where you created
--- `statuscolumn.lua`.
require("statuscolumn").setup();
Just like statuscolumn items
, we can have our own components
that do various things.
So, we will try making some simple components such as,
- Line number
- Fold
- Signs
- Separator

First, let's tackle the main purpose of a statuscolumn
. The line numbers!
We will use a combination of relative line numbers & absolute line numbers.
--- configuration for line number component.
---@class statuscolumn.lnum
---
--- Component identifier.
---@field kind "lnum"
---
--- Highlight group for absolute line numbers.
---@field hl? string
---
--- Highlight group for relative line numbers.
---@field rel_hl? string
--- Shows the line number.
---@param buffer integer
---@param config statuscolumn.lnum
---@return string
statuscolumn.lnum = function (buffer, _, config)
---@type integer Maximum number of lines in the buffer.
local line_count = vim.api.nvim_buf_line_count(buffer);
local num = vim.v.relnum == 0 and vim.v.lnum or vim.v.relnum;
local hl = vim.v.relnum == 0 and config.hl or config.rel_hl;
return table.concat({
set_hl(hl),
string.rep(" ", #tostring(line_count) - #tostring(num)),
num
});
end
Tip
You can use the __to_line
function that was shown above with this!
return table.concat({
set_hl(hl),
"%@v:lua.__to_line@",
string.rep(" ", #tostring(line_count) - #tostring(num)),
num,
"%X"
});
Let's add this component to the configuration.
statuscolumn.config = {
--- Other components.
{
kind = "lnum",
hl = "Special",
rel_hl = "Comment"
},
};

Folding is one of my most-used feature of Vim(& Neovim). So, a fold column is quite helpful for me.
Unfortunately, the API function for folds is quite limited. So, instead we will use FFI
(Foreign Function Interface) to access the internal functions instead.
local FFI = require("ffi");
--- Lines of C code.
---@type string[]
local C = {
"typedef struct {} Error;",
"typedef struct {} win;",
"typedef struct {",
" int start;",
" int level;",
" int llevel;",
" int lines;",
"} foldinfo;",
"win *find_window_by_handle(int window, Error *err);",
"foldinfo fold_info(win* wp, int lnum);",
};
FFI.cdef(table.concat(C, "\n"));
Now, let's create the function itself.
---@class statuscolumn.folds
---
---@field kind "folds"
---
--- Text for close folds.
---@field close_text string
---@field close_hl? string
---
---@field open_text string
---@field open_hl? string
---
---@field scope_end_text string
---@field scope_end_hl? string
---
---@field scope_merge_text string
---@field scope_merge_hl? string
---
---@field scope_text string
---@field scope_hl? string
---
---@field fill_text string
---@field fill_hl? string
---@param buffer integer
---@param window integer
---@param config statuscolumn.folds
statuscolumn.folds = function (buffer, window, config)
---@type integer
local window_handle = FFI.C.find_window_by_handle(window, nil);
---@type integer
local nlnum = math.min(vim.v.lnum + 1, vim.api.nvim_buf_line_count(buffer));
---@class fold_info
---
---@field start integer Start of the fold.
---@field level integer Level of the fold.
---@field llevel integer Highest level inside the fold.
---@field lines integer Number of lines a closed fold contains.
local info = FFI.C.fold_info(window_handle, vim.v.lnum);
---@type fold_info
local Ninfo = FFI.C.fold_info(window_handle, nlnum);
local closed_fold = false;
vim.api.nvim_buf_call(buffer, function ()
closed_fold = vim.fn.foldclosed(vim.v.lnum) ~= -1;
end);
local _o = "";
if info.start == vim.v.lnum then
--- Start of a fold.
if closed_fold == true then
--- Closed fold.
_o = table.concat({
_o,
set_hl(config.close_hl),
config.close_text or ""
});
else
--- Open fold.
_o = table.concat({
_o,
set_hl(config.open_hl),
config.open_text or ""
});
end
elseif info.level >= 1 and vim.v.lnum == vim.api.nvim_buf_line_count(buffer) then
--- Last line of a buffer.
_o = table.concat({
_o,
set_hl(config.scope_end_hl),
config.scope_end_text or ""
});
elseif info.start ~= Ninfo.start then
--- Last line of a fold.
if Ninfo.level == 0 then
--- End of the fold.
_o = table.concat({
_o,
set_hl(config.scope_end_hl),
config.scope_end_text or ""
});
elseif info.level == Ninfo.level then
--- End of the fold.
---
--- Next line has a fold
--- whose level is >=
--- to this one.
_o = table.concat({
_o,
set_hl(config.scope_end_hl),
config.scope_end_text or ""
});
elseif info.level > Ninfo.level then
--- End of the fold.
---
--- Next line has a fold
--- whose level is >=
--- to this one.
_o = table.concat({
_o,
set_hl(config.scope_merge_hl),
config.scope_merge_text or ""
});
elseif info.level > 0 then
_o = table.concat({
_o,
set_hl(config.scope_hl),
config.scope_text or ""
});
else
_o = table.concat({
_o,
set_hl(config.fill_hl),
config.fill_text or ""
});
end
elseif info.level > 0 then
_o = table.concat({
_o,
set_hl(config.scope_hl),
config.scope_text or ""
});
else
_o = table.concat({
_o,
set_hl(config.fill_hl),
config.fill_text or ""
});
end
return _o;
end
It may look kinda confusing, cause it kinda is. But long story short, we just compare the current line's fold information(level, start, end) with the next lines fold information.
Let's add this component to the configuration.
statuscolumn.config = {
{
kind = "folds",
close_text = "",
open_text = "",
scope_text = "│",
scope_end_text = "┗",
scope_merge_text = "┝",
fill_text = " "
},
--- Other components.
};

For signs, we will not do anything complicated.
---@class statuscolumn.signs
---
---@field kind "signs"
--- The sign column.
---@return string
statuscolumn.signs = function ()
return "%s";
end
We add it before the fold column.
statuscolumn.config = {
{
kind = "signs"
},
--- Other components.
};
Finally, we will create a Separator between different columns.
---@class statuscolumn.separator
---
---@field kind "separator"
---
--- Text to use as separator.
---@field text? string
---
--- Width of the separator.
---@field width? integer
---
--- Highlight group for the separator.
---@field hl? string
---@param config statuscolumn.separator
statuscolumn.separator = function (_, _, config)
return table.concat({
set_hl(config.hl),
string.rep(config.text or " ", config.width or 1)
});
end
Let's add some separators to our config!
statuscolumn.config = {
{
kind = "separator"
},
{
kind = "signs"
},
{
kind = "folds",
close_text = "",
open_text = "",
scope_text = "│",
scope_end_text = "┗",
scope_merge_text = "┝",
fill_text = " "
},
{
kind = "lnum",
hl = "Special",
rel_hl = "Comment"
},
{
kind = "separator"
},
{
kind = "separator",
text = "▍",
hl = "Comment"
},
};
And we are done!
You can check more complex components, per-window configuration, click handling etc. in the source file.
-
plugin/bars.lua Shows how to set up per-window statuscolumn.
-
lua/bars/statuscolumn.lua Shows how to manage per-window configuration, set & reset the statusline of a window and rendering statuscolumn.
-
lua/bars/components/statuscolumn.lua Shows how various components are made.
- lua/definitions/statuscolumn.lua Shows type definitions for the statuscolumn.
Also available in Vimdoc, :h bars.nvim-statuscolumn
.