mirror of
https://github.com/lewis6991/gitsigns.nvim
synced 2025-02-20 22:47:23 +00:00
- 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.
379 lines
8.7 KiB
Lua
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
|