gitsigns.nvim/lua/gitsigns/util.lua
Lewis Russell 92a8fbb845 feat!: remove current_line_blame_formatter_opts
- Also make relative time the default time format for current_line_blame_formatter
- When `current_line_blame_formatter` is passed as a function it is no
  longer passed an opts argument.
2024-06-19 14:51:26 +01:00

379 lines
8.7 KiB
Lua

local M = {}
function M.path_exists(path)
return vim.loop.fs_stat(path) and true or false
end
local jit_os --- @type string
if jit then
jit_os = jit.os:lower()
end
local is_unix = false
if jit_os then
is_unix = jit_os == 'linux' or jit_os == 'osx' or jit_os == 'bsd'
else
local binfmt = package.cpath:match('%p[\\|/]?%p(%a+)')
is_unix = binfmt ~= 'dll'
end
--- @param file string
--- @return string
function M.dirname(file)
return file:match(string.format('^(.+)%s[^%s]+', M.path_sep, M.path_sep))
end
--- @param path string
--- @return string[]
function M.file_lines(path)
local file = assert(io.open(path, 'rb'))
local contents = file:read('*a')
file:close()
return vim.split(contents, '\n')
end
M.path_sep = package.config:sub(1, 1)
--- @param ... integer
--- @return string
local function make_bom(...)
local r = {}
---@diagnostic disable-next-line:no-unknown
for i, a in ipairs({ ... }) do
---@diagnostic disable-next-line:no-unknown
r[i] = string.char(a)
end
return table.concat(r)
end
local BOM_TABLE = {
['utf-8'] = make_bom(0xef, 0xbb, 0xbf),
['utf-16le'] = make_bom(0xff, 0xfe),
['utf-16'] = make_bom(0xfe, 0xff),
['utf-16be'] = make_bom(0xfe, 0xff),
['utf-32le'] = make_bom(0xff, 0xfe, 0x00, 0x00),
['utf-32'] = make_bom(0xff, 0xfe, 0x00, 0x00),
['utf-32be'] = make_bom(0x00, 0x00, 0xfe, 0xff),
['utf-7'] = make_bom(0x2b, 0x2f, 0x76),
['utf-1'] = make_bom(0xf7, 0x54, 0x4c),
}
---@param x string
---@param encoding string
---@return string
local function add_bom(x, encoding)
local bom = BOM_TABLE[encoding]
if bom then
return bom .. x
end
return x
end
--- @param bufnr integer
--- @param noendofline? boolean
--- @return string[]
function M.buf_lines(bufnr, noendofline)
-- nvim_buf_get_lines strips carriage returns if fileformat==dos
local buftext = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local dos = vim.bo[bufnr].fileformat == 'dos'
if dos then
for i = 1, #buftext - 1 do
buftext[i] = buftext[i] .. '\r'
end
end
if not noendofline and vim.bo[bufnr].endofline then
-- Add CR to the last line
if dos then
buftext[#buftext] = buftext[#buftext] .. '\r'
end
buftext[#buftext + 1] = ''
end
if vim.bo[bufnr].bomb then
buftext[1] = add_bom(buftext[1], vim.bo[bufnr].fileencoding)
end
return buftext
end
--- @param buf integer
local function delete_alt(buf)
local alt = vim.api.nvim_buf_call(buf, function()
return vim.fn.bufnr('#')
end)
if alt ~= buf and alt ~= -1 then
pcall(vim.api.nvim_buf_delete, alt, { force = true })
end
end
--- @param bufnr integer
--- @param name string
function M.buf_rename(bufnr, name)
vim.api.nvim_buf_set_name(bufnr, name)
delete_alt(bufnr)
end
--- @param events string[]
--- @param f fun()
function M.noautocmd(events, f)
local ei = vim.o.eventignore
vim.o.eventignore = table.concat(events, ',')
f()
vim.o.eventignore = ei
end
--- @param bufnr integer
--- @param start_row integer
--- @param end_row integer
--- @param lines string[]
function M.set_lines(bufnr, start_row, end_row, lines)
if vim.bo[bufnr].fileformat == 'dos' then
lines = M.strip_cr(lines)
end
if start_row == 0 and end_row == -1 and lines[#lines] == '' then
lines = vim.deepcopy(lines)
lines[#lines] = nil
end
vim.api.nvim_buf_set_lines(bufnr, start_row, end_row, false, lines)
end
--- @return string
function M.tmpname()
if is_unix then
return os.tmpname()
end
return vim.fn.tempname()
end
--- @param time number
--- @param divisor integer
--- @param time_word string
--- @return string
local function to_relative_string(time, divisor, time_word)
local num = math.floor(time / divisor)
if num > 1 then
time_word = time_word .. 's'
end
return num .. ' ' .. time_word .. ' ago'
end
--- @param timestamp number
--- @return string
function M.get_relative_time(timestamp)
local current_timestamp = os.time()
local elapsed = current_timestamp - timestamp
if elapsed == 0 then
return 'a while ago'
end
local minute_seconds = 60
local hour_seconds = minute_seconds * 60
local day_seconds = hour_seconds * 24
local month_seconds = day_seconds * 30
local year_seconds = month_seconds * 12
if elapsed < minute_seconds then
return to_relative_string(elapsed, 1, 'second')
elseif elapsed < hour_seconds then
return to_relative_string(elapsed, minute_seconds, 'minute')
elseif elapsed < day_seconds then
return to_relative_string(elapsed, hour_seconds, 'hour')
elseif elapsed < month_seconds then
return to_relative_string(elapsed, day_seconds, 'day')
elseif elapsed < year_seconds then
return to_relative_string(elapsed, month_seconds, 'month')
else
return to_relative_string(elapsed, year_seconds, 'year')
end
end
--- @param opts vim.api.keyset.redraw
function M.redraw(opts)
if vim.fn.has('nvim-0.10') == 1 then
vim.api.nvim__redraw(opts)
else
vim.api.nvim__buf_redraw_range(opts.buf, opts.range[1], opts.range[2])
end
end
--- @param xs string[]
--- @return boolean
local function is_dos(xs)
-- Do not check CR at EOF
for i = 1, #xs - 1 do
if xs[i]:sub(-1) ~= '\r' then
return false
end
end
return true
end
--- Strip '\r' from the EOL of each line only if all lines end with '\r'
--- @param xs0 string[]
--- @return string[]
function M.strip_cr(xs0)
if not is_dos(xs0) then
-- don't strip, return early
return xs0
end
-- all lines end with '\r', need to strip
local xs = vim.deepcopy(xs0)
for i = 1, #xs do
xs[i] = xs[i]:sub(1, -2)
end
return xs
end
--- @param base? string
--- @return string?
function M.norm_base(base)
if base == ':0' then
return
end
if base and base:sub(1, 1):match('[~\\^]') then
base = 'HEAD' .. base
end
return base
end
function M.emptytable()
return setmetatable({}, {
---@param t table<any,any>
---@param k any
---@return any
__index = function(t, k)
t[k] = {}
return t[k]
end,
})
end
local function expand_date(fmt, time)
if fmt == '%R' then
return M.get_relative_time(time)
end
return os.date(fmt, time)
end
---@param fmt string
---@param info table<string,any>
---@return string
function M.expand_format(fmt, info)
local ret = {} --- @type string[]
for _ = 1, 20 do -- loop protection
-- Capture <name> or <name:format>
local scol, ecol, match, key, time_fmt = fmt:find('(<([^:>]+):?([^>]*)>)')
if not match then
break
end
--- @cast key string
ret[#ret + 1], fmt = fmt:sub(1, scol - 1), fmt:sub(ecol + 1)
local v = info[key]
if v then
if type(v) == 'table' then
v = table.concat(v, '\n')
end
if vim.endswith(key, '_time') then
if time_fmt == '' then
time_fmt = '%Y-%m-%d'
end
v = expand_date(time_fmt, v)
end
match = tostring(v)
end
ret[#ret + 1] = match
end
ret[#ret + 1] = fmt
return table.concat(ret, '')
end
--- @param buf string
--- @return boolean
function M.bufexists(buf)
--- @diagnostic disable-next-line:param-type-mismatch
return vim.fn.bufexists(buf) == 1
end
--- @param x Gitsigns.BlameInfo
--- @return Gitsigns.BlameInfoPublic
function M.convert_blame_info(x)
--- @type Gitsigns.BlameInfoPublic
local ret = vim.tbl_extend('error', x, x.commit)
ret.commit = nil
return ret
end
--- Efficiently remove items from middle of a list a list.
---
--- Calling table.remove() in a loop will re-index the tail of the table on
--- every iteration, instead this function will re-index the table exactly
--- once.
---
--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
---
---@param t any[]
---@param first integer
---@param last integer
function M.list_remove(t, first, last)
local n = #t
for i = 0, n - first do
t[first + i] = t[last + 1 + i]
t[last + 1 + i] = nil
end
end
--- Efficiently insert items into the middle of a list.
---
--- Calling table.insert() in a loop will re-index the tail of the table on
--- every iteration, instead this function will re-index the table exactly
--- once.
---
--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
---
---@param t any[]
---@param first integer
---@param last integer
---@param v any
function M.list_insert(t, first, last, v)
local n = #t
-- Shift table forward
for i = n - first, 0, -1 do
t[last + 1 + i] = t[first + i]
end
-- Fill in new values
for i = first, last do
t[i] = v
end
end
--- Run a function once and ignore subsequent calls
--- @generic F: function
--- @param fn F
--- @return F
function M.once(fn)
local called = false
return function(...)
if called then
return
end
called = true
return fn(...)
end
end
return M