|
| 1 | +local ffi = require('ffi') |
| 2 | +local utils = require('utils') |
| 3 | + |
| 4 | +---Wrapper around `utils.stl.hl()` that forces apply hlgroup even in tty |
| 5 | +---@param str? string sign symbol |
| 6 | +---@param hl? string name of the highlight group |
| 7 | +---@param restore? boolean restore highlight after the sign, default true |
| 8 | +local function make_hl(str, hl, restore) |
| 9 | + return utils.stl.hl(str, hl, restore, true) |
| 10 | +end |
| 11 | + |
| 12 | +---@type table<integer, integer> |
| 13 | +local lnumw_cache = {} |
| 14 | + |
| 15 | +---@class stc_shared_data_t |
| 16 | +---@field win integer |
| 17 | +---@field wp ffi.cdata* winpos_T C struct for window attributes |
| 18 | +---@field display_tick? integer display tick |
| 19 | +---@field buf_tick? integer b:changedtick |
| 20 | +---@field buf? integer |
| 21 | +---@field lnumw? integer number of digits of the largest line number |
| 22 | +---@field nu? boolean &number |
| 23 | +---@field rnu? boolean &relativenumber |
| 24 | +---@field nuw? integer &numberwidth |
| 25 | +---@field scl? string &signcolumn |
| 26 | +---@field fdc? string &foldcolumn |
| 27 | +---@field show_nu? boolean whether to show line number (either &nu or &rnu is true) |
| 28 | +---@field show_scl? boolean whether to show sign column |
| 29 | +---@field show_fdc? boolean whether to show fold column |
| 30 | +---@field cur? integer[] cursor position |
| 31 | +---@field cul? boolean &cursorline |
| 32 | +---@field culopt? string &cursorlineopt |
| 33 | +---@field culhl? boolean whether to use cursorline highlight at current line |
| 34 | +---@field lnumabovehl? boolean whether to use LineNrAbove at current line in number column |
| 35 | +---@field lnumbelowhl? boolean whether to use LineNrBelow at current line in number column |
| 36 | +---@field foldopen? string fold open sign |
| 37 | +---@field foldclose? string fold close sign |
| 38 | +---@field foldsep? string fold separator sign |
| 39 | +---@field extsigns? extmark_sign_t[] extmark signs, see `:h extmarks` |
| 40 | +---@field lnum? integer v:lnum |
| 41 | +---@field relnum? integer v:relnum |
| 42 | +---@field virtnum? integer v:virtnum |
| 43 | + |
| 44 | +---@class extmark_sign_t |
| 45 | +---@field [1] integer extmark_id |
| 46 | +---@field [2] integer row, 0-indexed |
| 47 | +---@field [3] integer col, 0-indexed |
| 48 | +---@field [4] extmark_sign_details_t details |
| 49 | + |
| 50 | +---@class extmark_sign_details_t: vim.api.keyset.set_extmark |
| 51 | +---@field sign_name string? only set when sign is defined using legacy `sign_define()` |
| 52 | +---@field ns_id integer |
| 53 | + |
| 54 | +---Shared data in each window |
| 55 | +---@type table<string, stc_shared_data_t> |
| 56 | +local shared = {} |
| 57 | + |
| 58 | +---@type table<string, fun(data: stc_shared_data_t, ...): string> |
| 59 | +local builders = {} |
| 60 | + |
| 61 | +ffi.cdef([[ |
| 62 | + typedef struct {} Error; |
| 63 | + typedef struct {} win_T; |
| 64 | + typedef struct { |
| 65 | + int start; // line number where deepest fold starts |
| 66 | + int level; // fold level, when zero other fields are N/A |
| 67 | + int llevel; // lowest level that starts in v:lnum |
| 68 | + int lines; // number of lines from v:lnum to end of closed fold |
| 69 | + } foldinfo_T; |
| 70 | + foldinfo_T fold_info(win_T* wp, int lnum); |
| 71 | + win_T *find_window_by_handle(int Window, Error *err); |
| 72 | +
|
| 73 | + // Display tick, incremented for each call to update_screen() |
| 74 | + uint64_t display_tick; |
| 75 | +]]) |
| 76 | + |
| 77 | +---Returns the string representation of sign column to be shown |
| 78 | +---@param data stc_shared_data_t |
| 79 | +---@param filter fun(sign: extmark_sign_t, data: stc_shared_data_t): boolean |
| 80 | +---@param virtual boolean whether to draw sign in virtual line |
| 81 | +---@return string |
| 82 | +function builders.signcol(data, filter, virtual) |
| 83 | + if not data.show_scl then |
| 84 | + return '' |
| 85 | + end |
| 86 | + if data.virtnum ~= 0 and not virtual then |
| 87 | + goto signcol_ret_default |
| 88 | + end |
| 89 | + do |
| 90 | + ---@type extmark_sign_details_t? |
| 91 | + local sign_details |
| 92 | + for _, sign in ipairs(data.extsigns) do |
| 93 | + local lnum = sign[2] + 1 -- 0-indexed to 1-indexed |
| 94 | + local current_sign_details = sign[4] |
| 95 | + if lnum > data.lnum then |
| 96 | + break |
| 97 | + end |
| 98 | + if |
| 99 | + lnum == data.lnum |
| 100 | + and filter(sign, data) |
| 101 | + and current_sign_details.sign_text |
| 102 | + and ( |
| 103 | + not sign_details |
| 104 | + or current_sign_details.priority > sign_details.priority |
| 105 | + ) |
| 106 | + then |
| 107 | + sign_details = current_sign_details |
| 108 | + end |
| 109 | + end |
| 110 | + if sign_details then |
| 111 | + return make_hl( |
| 112 | + vim.trim(sign_details.sign_text), |
| 113 | + data.culhl and sign_details.cursorline_hl_group |
| 114 | + or sign_details.sign_hl_group --[[@as string]] |
| 115 | + ) |
| 116 | + end |
| 117 | + end |
| 118 | + ::signcol_ret_default:: |
| 119 | + return make_hl(' ', data.culhl and 'CursorLineSign' or 'SignColumn') |
| 120 | +end |
| 121 | + |
| 122 | +---@param data stc_shared_data_t |
| 123 | +---@return string |
| 124 | +function builders.lnum(data) |
| 125 | + local result = '' ---@type string|integer |
| 126 | + if not data.show_nu then |
| 127 | + return '' |
| 128 | + end |
| 129 | + if data.virtnum ~= 0 then -- Drawing virtual line |
| 130 | + goto lnum_ret_default |
| 131 | + end |
| 132 | + if not data.nu then |
| 133 | + result = data.relnum |
| 134 | + goto lnum_ret_default |
| 135 | + end |
| 136 | + if not data.rnu then |
| 137 | + result = data.lnum |
| 138 | + goto lnum_ret_default |
| 139 | + end |
| 140 | + if data.relnum == 0 then |
| 141 | + return string.format( |
| 142 | + '%%=%-' .. math.max(data.nuw - 1, data.lnumw or 0) .. 'd ', |
| 143 | + data.lnum |
| 144 | + ) |
| 145 | + end |
| 146 | + result = data.relnum |
| 147 | + |
| 148 | + ::lnum_ret_default:: |
| 149 | + return string.format( |
| 150 | + '%%=%' .. math.max(data.nuw - 1, data.lnumw or 0) .. 's ', |
| 151 | + result |
| 152 | + ) |
| 153 | +end |
| 154 | + |
| 155 | +---@param data stc_shared_data_t |
| 156 | +---@return string |
| 157 | +function builders.foldcol(data) |
| 158 | + if not data.show_fdc then |
| 159 | + return '' |
| 160 | + end |
| 161 | + local lnum = data.lnum --[[@as integer]] |
| 162 | + local foldinfo = ffi.C.fold_info(data.wp, lnum) |
| 163 | + local foldchar = (data.virtnum ~= 0 or foldinfo.start ~= lnum) |
| 164 | + and data.foldsep |
| 165 | + or foldinfo.lines == 0 and data.foldopen |
| 166 | + or data.foldclose |
| 167 | + return make_hl(foldchar, data.culhl and 'CursorLineFold' or 'FoldColumn') |
| 168 | +end |
| 169 | + |
| 170 | +---Get a valid name of an extmark sign |
| 171 | +---@param sign extmark_sign_t |
| 172 | +---@return string |
| 173 | +local function extsign_get_name(sign) |
| 174 | + local details = sign[4] |
| 175 | + return details.sign_name or details.sign_hl_group or '' --[[@as string]] |
| 176 | +end |
| 177 | + |
| 178 | +---@param sign extmark_sign_t |
| 179 | +---@param data stc_shared_data_t |
| 180 | +---@return boolean |
| 181 | +local function gitsigns_filter(sign, data) |
| 182 | + local name = extsign_get_name(sign) |
| 183 | + if not name:find('^Git') then |
| 184 | + return false |
| 185 | + end |
| 186 | + if data.virtnum ~= 0 then -- virtual lines, not showing git delete signs |
| 187 | + return not name:find('[Dd]elete$') |
| 188 | + end |
| 189 | + return true |
| 190 | +end |
| 191 | + |
| 192 | +---@param sign extmark_sign_t |
| 193 | +---@return boolean |
| 194 | +local function nongitsigns_filter(sign) |
| 195 | + return not extsign_get_name(sign):find('^Git') |
| 196 | +end |
| 197 | + |
| 198 | +---Get number of digits of a decimal integer |
| 199 | +---@param number integer |
| 200 | +---@return integer |
| 201 | +local function numdigits(number) |
| 202 | + local result = 0 |
| 203 | + while number >= 1 do |
| 204 | + number = number / 10 |
| 205 | + result = result + 1 |
| 206 | + end |
| 207 | + return result |
| 208 | +end |
| 209 | + |
| 210 | +---@return string |
| 211 | +function _G._stc() |
| 212 | + local win = vim.g.statusline_winid |
| 213 | + local display_tick = ffi.C.display_tick --[[@as uinteger]] |
| 214 | + if not shared[win] then -- Initialize shared data |
| 215 | + shared[win] = { |
| 216 | + win = win, |
| 217 | + wp = ffi.C.find_window_by_handle(win, ffi.new('Error')), |
| 218 | + } |
| 219 | + end |
| 220 | + |
| 221 | + local data = shared[win] |
| 222 | + if not data.display_tick or data.display_tick < display_tick then -- Update shared data |
| 223 | + local wo = vim.wo[win] |
| 224 | + local fcs = vim.opt_local.fillchars:get() |
| 225 | + local buf = vim.api.nvim_win_get_buf(win) |
| 226 | + local wininfo = vim.fn.getwininfo(win)[1] |
| 227 | + data.display_tick = display_tick |
| 228 | + data.buf = buf |
| 229 | + data.cur = vim.api.nvim_win_get_cursor(win) |
| 230 | + data.cul = wo.cul |
| 231 | + data.culopt = wo.culopt |
| 232 | + data.nu = wo.nu |
| 233 | + data.rnu = wo.rnu |
| 234 | + data.nuw = wo.nuw |
| 235 | + data.scl = wo.scl |
| 236 | + data.fdc = wo.fdc |
| 237 | + data.show_nu = data.nu or data.rnu |
| 238 | + data.show_scl = data.scl ~= 'no' |
| 239 | + data.show_fdc = data.fdc ~= '0' |
| 240 | + data.foldopen = fcs.foldopen or '-' |
| 241 | + data.foldclose = fcs.foldclose or '+' |
| 242 | + data.foldsep = fcs.foldsep or '|' |
| 243 | + data.extsigns = vim.api.nvim_buf_get_extmarks( |
| 244 | + buf, |
| 245 | + -1, |
| 246 | + { wininfo.topline - 1, 0 }, |
| 247 | + { wininfo.botline - 1, -1 }, |
| 248 | + { |
| 249 | + type = 'sign', |
| 250 | + details = true, |
| 251 | + } |
| 252 | + ) |
| 253 | + |
| 254 | + -- lnum width is only needed when both &nu and &rnu are enabled |
| 255 | + if data.nu and data.rnu then |
| 256 | + local buf_tick = vim.api.nvim_buf_get_changedtick(buf) |
| 257 | + if not data.buf_tick or data.buf_tick < buf_tick then |
| 258 | + lnumw_cache[buf] = numdigits(vim.api.nvim_buf_line_count(buf)) |
| 259 | + data.buf_tick = buf_tick |
| 260 | + end |
| 261 | + -- Cache could be nil after BufDelete |
| 262 | + data.lnumw = lnumw_cache[buf] or data.lnumw |
| 263 | + end |
| 264 | + end |
| 265 | + |
| 266 | + data.lnum = vim.v.lnum |
| 267 | + data.relnum = vim.v.relnum |
| 268 | + data.virtnum = vim.v.virtnum |
| 269 | + |
| 270 | + data.culhl = data.cul |
| 271 | + and data.culopt:find('[ou]') |
| 272 | + and data.lnum == data.cur[1] |
| 273 | + |
| 274 | + return builders.signcol(data, nongitsigns_filter) |
| 275 | + .. (data.show_scl and ' ' or '') |
| 276 | + .. builders.lnum(data) |
| 277 | + .. builders.signcol(data, gitsigns_filter, true) |
| 278 | + .. builders.foldcol(data) |
| 279 | + .. (data.show_fdc and ' ' or '') |
| 280 | +end |
| 281 | + |
| 282 | +---@return nil |
| 283 | +local function setup() |
| 284 | + if vim.g.loaded_statuscolumn ~= nil then |
| 285 | + return |
| 286 | + end |
| 287 | + vim.g.loaded_statuscolumn = true |
| 288 | + |
| 289 | + ---Attach statuscolumn to current window |
| 290 | + local function _attach() |
| 291 | + if |
| 292 | + vim.bo.bt == '' |
| 293 | + and vim.wo.stc == '' |
| 294 | + and vim.fn.win_gettype() == '' |
| 295 | + and not vim.b.bigfile |
| 296 | + then |
| 297 | + vim.opt_local.stc = '%!v:lua._stc()' |
| 298 | + end |
| 299 | + end |
| 300 | + |
| 301 | + for _, win in ipairs(vim.api.nvim_list_wins()) do |
| 302 | + vim.api.nvim_win_call(win, _attach) |
| 303 | + end |
| 304 | + |
| 305 | + local augroup = vim.api.nvim_create_augroup('StatusColumn', {}) |
| 306 | + vim.api.nvim_create_autocmd({ 'BufWritePost', 'BufWinEnter' }, { |
| 307 | + group = augroup, |
| 308 | + desc = 'Set statuscolumn for each window.', |
| 309 | + callback = function() |
| 310 | + _attach() |
| 311 | + end, |
| 312 | + }) |
| 313 | + vim.api.nvim_create_autocmd('WinClosed', { |
| 314 | + group = augroup, |
| 315 | + desc = 'Clear per window shared data cache.', |
| 316 | + callback = function(info) |
| 317 | + shared[tonumber(info.match)] = nil |
| 318 | + end, |
| 319 | + }) |
| 320 | + vim.api.nvim_create_autocmd('BufDelete', { |
| 321 | + group = augroup, |
| 322 | + desc = 'Clear per buffer lnum width cache.', |
| 323 | + callback = function(info) |
| 324 | + lnumw_cache[info.buf] = nil |
| 325 | + end, |
| 326 | + }) |
| 327 | +end |
| 328 | + |
| 329 | +return { |
| 330 | + setup = setup, |
| 331 | +} |
0 commit comments