464 lines
12 KiB
Lua
464 lines
12 KiB
Lua
local Job = require("plenary.job")
|
|
local M = {}
|
|
|
|
M.get_colors_for_group = function(group)
|
|
local normal_fg = vim.fn.synIDattr(vim.fn.synIDtrans((vim.fn.hlID(group))), "fg")
|
|
local normal_bg = vim.fn.synIDattr(vim.fn.synIDtrans((vim.fn.hlID(group))), "bg")
|
|
return { fg = normal_fg, bg = normal_bg }
|
|
end
|
|
|
|
M.get_current_line_number = function()
|
|
return vim.api.nvim_call_function("line", { "." })
|
|
end
|
|
|
|
M.has_reviewer = function(reviewer)
|
|
if reviewer == "diffview" then
|
|
local diffview_ok, _ = pcall(require, "diffview")
|
|
if not diffview_ok then
|
|
error("Please install diffview or change your reviewer")
|
|
end
|
|
else
|
|
local has_reviewer = vim.fn.executable("delta") == 1
|
|
if not has_reviewer then
|
|
error(string.format("Please install delta or change your reviewer", reviewer))
|
|
end
|
|
end
|
|
end
|
|
|
|
M.is_windows = function()
|
|
if vim.fn.has("win32") == 1 or vim.fn.has("win32unix") == 1 then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
M.P = function(...)
|
|
local objects = {}
|
|
for i = 1, select("#", ...) do
|
|
local v = select(i, ...)
|
|
table.insert(objects, vim.inspect(v))
|
|
end
|
|
|
|
print(table.concat(objects, "\n"))
|
|
return ...
|
|
end
|
|
|
|
M.get_buffer_text = function(bufnr)
|
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
local text = table.concat(lines, "\n")
|
|
return text
|
|
end
|
|
|
|
M.string_starts = function(str, start)
|
|
return str:sub(1, #start) == start
|
|
end
|
|
|
|
M.press_enter = function()
|
|
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<CR>", false, true, true), "n", false)
|
|
end
|
|
|
|
M.format_date = function(date_string)
|
|
local date_table = os.date("!*t")
|
|
local year, month, day, hour, min, sec = date_string:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)")
|
|
local date = os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec })
|
|
|
|
local current_date = os.time({
|
|
year = date_table.year,
|
|
month = date_table.month,
|
|
day = date_table.day,
|
|
hour = date_table.hour,
|
|
min = date_table.min,
|
|
sec = date_table.sec,
|
|
})
|
|
|
|
local time_diff = current_date - date
|
|
|
|
local function pluralize(num, word)
|
|
return num .. string.format(" %s", word) .. (num > 1 and "s" or "") .. " ago"
|
|
end
|
|
|
|
if time_diff < 60 then
|
|
return pluralize(time_diff, "second")
|
|
elseif time_diff < 3600 then
|
|
return pluralize(math.floor(time_diff / 60), "minute")
|
|
elseif time_diff < 86400 then
|
|
return pluralize(math.floor(time_diff / 3600), "hour")
|
|
elseif time_diff < 2592000 then
|
|
return pluralize(math.floor(time_diff / 86400), "day")
|
|
else
|
|
local formatted_date = os.date("%A, %B %e", date)
|
|
return formatted_date
|
|
end
|
|
end
|
|
|
|
M.jump_to_file = function(filename, line_number)
|
|
if line_number == nil then
|
|
line_number = 1
|
|
end
|
|
local bufnr = vim.fn.bufnr(filename)
|
|
if bufnr ~= -1 then
|
|
M.jump_to_buffer(bufnr, line_number)
|
|
return
|
|
end
|
|
|
|
-- If buffer is not already open, open it
|
|
vim.cmd("edit " .. filename)
|
|
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
|
|
end
|
|
|
|
M.jump_to_buffer = function(bufnr, line_number)
|
|
vim.cmd("buffer " .. bufnr)
|
|
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
|
|
end
|
|
|
|
M.create_popup_state = function(title, width, height)
|
|
return {
|
|
buf_options = {
|
|
filetype = "markdown",
|
|
},
|
|
relative = "editor",
|
|
enter = true,
|
|
focusable = true,
|
|
border = {
|
|
style = "rounded",
|
|
text = {
|
|
top = title,
|
|
},
|
|
},
|
|
position = "50%",
|
|
size = {
|
|
width = width,
|
|
height = height,
|
|
},
|
|
}
|
|
end
|
|
|
|
M.merge = function(defaults, overrides)
|
|
if type(defaults) == "table" and M.table_size(defaults) == 0 and type(overrides) == "table" then
|
|
return overrides
|
|
end
|
|
return vim.tbl_deep_extend("force", defaults, overrides)
|
|
end
|
|
|
|
M.join = function(tbl, separator)
|
|
separator = separator or " "
|
|
|
|
local result = ""
|
|
for _, value in pairs(tbl) do
|
|
result = result .. tostring(value) .. separator
|
|
end
|
|
|
|
-- Remove the trailing separator
|
|
if separator ~= "" then
|
|
result = result:sub(1, -#separator - 1)
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
M.remove_first_value = function(tbl)
|
|
local sliced_table = {}
|
|
for i = 2, #tbl do
|
|
table.insert(sliced_table, tbl[i])
|
|
end
|
|
|
|
return sliced_table
|
|
end
|
|
|
|
M.read_file = function(file_path)
|
|
local file = io.open(file_path, "r")
|
|
if file == nil then
|
|
return nil
|
|
end
|
|
local file_contents = file:read("*all")
|
|
file:close()
|
|
file_contents = string.gsub(file_contents, "\n", "")
|
|
return file_contents
|
|
end
|
|
|
|
M.current_file_path = function()
|
|
local path = debug.getinfo(1, "S").source:sub(2)
|
|
return vim.fn.fnamemodify(path, ":p")
|
|
end
|
|
|
|
local random = math.random
|
|
M.uuid = function()
|
|
local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
|
|
return string.gsub(template, "[xy]", function(c)
|
|
local v = (c == "x") and random(0, 0xf) or random(8, 0xb)
|
|
return string.format("%x", v)
|
|
end)
|
|
end
|
|
|
|
M.join_tables = function(table1, table2)
|
|
for _, value in ipairs(table2) do
|
|
table.insert(table1, value)
|
|
end
|
|
|
|
return table1
|
|
end
|
|
|
|
M.table_size = function(t)
|
|
local count = 0
|
|
for _ in pairs(t) do
|
|
count = count + 1
|
|
end
|
|
return count
|
|
end
|
|
|
|
M.contains = function(array, search_value)
|
|
for _, value in ipairs(array) do
|
|
if value == search_value then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
M.extract = function(t, property)
|
|
local resultTable = {}
|
|
for _, value in ipairs(t) do
|
|
if value[property] then
|
|
table.insert(resultTable, value[property])
|
|
end
|
|
end
|
|
return resultTable
|
|
end
|
|
|
|
M.remove_last_chunk = function(sentence)
|
|
local words = {}
|
|
for word in sentence:gmatch("%S+") do
|
|
table.insert(words, word)
|
|
end
|
|
table.remove(words, #words)
|
|
local sentence_without_last = table.concat(words, " ")
|
|
return sentence_without_last
|
|
end
|
|
|
|
M.get_first_chunk = function(sentence, divider)
|
|
local words = {}
|
|
for word in sentence:gmatch(divider or "%S+") do
|
|
table.insert(words, word)
|
|
end
|
|
return words[1]
|
|
end
|
|
|
|
M.get_last_chunk = function(sentence, divider)
|
|
local words = {}
|
|
for word in sentence:gmatch(divider or "%S+") do
|
|
table.insert(words, word)
|
|
end
|
|
return words[#words]
|
|
end
|
|
|
|
M.trim = function(s)
|
|
return s:gsub("^%s+", ""):gsub("%s+$", "")
|
|
end
|
|
|
|
M.get_line_content = function(bufnr, start)
|
|
local current_buffer = vim.api.nvim_get_current_buf()
|
|
local lines = vim.api.nvim_buf_get_lines(bufnr ~= nil and bufnr or current_buffer, start - 1, start, false)
|
|
return lines[1]
|
|
end
|
|
|
|
M.get_win_from_buf = function(bufnr)
|
|
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
|
if vim.fn.winbufnr(win) == bufnr then
|
|
return win
|
|
end
|
|
end
|
|
end
|
|
|
|
M.switch_can_edit_buf = function(buf, bool)
|
|
vim.api.nvim_set_option_value("modifiable", bool, { buf = buf })
|
|
vim.api.nvim_set_option_value("readonly", not bool, { buf = buf })
|
|
end
|
|
|
|
M.list_files_in_folder = function(folder_path)
|
|
if vim.fn.isdirectory(folder_path) == 0 then
|
|
return nil
|
|
end
|
|
|
|
local folder_ok, folder = pcall(vim.fn.readdir, folder_path)
|
|
|
|
if not folder_ok then
|
|
return nil
|
|
end
|
|
|
|
local files = {}
|
|
if folder ~= nil then
|
|
for _, file in ipairs(folder) do
|
|
local file_path = folder_path .. (M.is_windows() and "\\" or "/") .. file
|
|
local timestamp = vim.fn.getftime(file_path)
|
|
table.insert(files, { name = file, timestamp = timestamp })
|
|
end
|
|
end
|
|
|
|
-- Sort the table by timestamp in descending order (newest first)
|
|
table.sort(files, function(a, b)
|
|
return a.timestamp > b.timestamp
|
|
end)
|
|
|
|
local result = {}
|
|
for _, file in ipairs(files) do
|
|
table.insert(result, file.name)
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
M.reverse = function(list)
|
|
local rev = {}
|
|
for i = #list, 1, -1 do
|
|
rev[#rev + 1] = list[i]
|
|
end
|
|
return rev
|
|
end
|
|
|
|
---@class Hunk
|
|
---@field old_line integer
|
|
---@field old_range integer
|
|
---@field new_line integer
|
|
---@field new_range integer
|
|
|
|
---Parse git diff hunks.
|
|
---@param file_path string Path to file.
|
|
---@param base_branch string Git base branch of merge request.
|
|
---@return Hunk[] list of hunks.
|
|
M.parse_hunk_headers = function(file_path, base_branch)
|
|
local hunks = {}
|
|
|
|
local diff_job = Job:new({
|
|
command = "git",
|
|
args = { "diff", "--minimal", "--unified=0", "--no-color", base_branch, "--", file_path },
|
|
on_exit = function(j, return_code)
|
|
if return_code == 0 then
|
|
for _, line in ipairs(j:result()) do
|
|
if line:sub(1, 2) == "@@" then
|
|
-- match:
|
|
-- @@ -23 +23 @@ ...
|
|
-- @@ -23,0 +23 @@ ...
|
|
-- @@ -41,0 +42,4 @@ ...
|
|
local old_start, old_range, new_start, new_range = line:match("@@+ %-(%d+),?(%d*) %+(%d+),?(%d*) @@+")
|
|
|
|
table.insert(hunks, {
|
|
old_line = tonumber(old_start),
|
|
old_range = tonumber(old_range) or 0,
|
|
new_line = tonumber(new_start),
|
|
new_range = tonumber(new_range) or 0,
|
|
})
|
|
end
|
|
end
|
|
else
|
|
vim.notify("Failed to get git diff: " .. j:stderr(), vim.log.levels.WARN)
|
|
end
|
|
end,
|
|
})
|
|
|
|
diff_job:sync()
|
|
|
|
return hunks
|
|
end
|
|
|
|
---@class LineDiffInfo
|
|
---@field old_line integer
|
|
---@field new_line integer
|
|
---@field in_hunk boolean
|
|
|
|
---Search git diff hunks to find old and new line number corresponding to target line.
|
|
---This function does not check if target line is outside of boundaries of file.
|
|
---@param hunks Hunk[] git diff parsed hunks.
|
|
---@param target_line integer line number to search for - based on is_new paramter the search is
|
|
---either in new lines or old lines of hunks.
|
|
---@param is_new boolean whether to search for new line or old line
|
|
---@return LineDiffInfo
|
|
M.get_lines_from_hunks = function(hunks, target_line, is_new)
|
|
if #hunks == 0 then
|
|
-- If there are zero hunks, return target_line for both old and new lines
|
|
return { old_line = target_line, new_line = target_line, in_hunk = false }
|
|
end
|
|
local current_new_line = 0
|
|
local current_old_line = 0
|
|
if is_new then
|
|
for _, hunk in ipairs(hunks) do
|
|
-- target line is before current hunk
|
|
if target_line < hunk.new_line then
|
|
return {
|
|
old_line = current_old_line + (target_line - current_new_line),
|
|
new_line = target_line,
|
|
in_hunk = false,
|
|
}
|
|
-- target line is within the current hunk
|
|
elseif hunk.new_line <= target_line and target_line <= (hunk.new_line + hunk.new_range) then
|
|
-- this is interesting magic of gitlab calculation
|
|
return {
|
|
old_line = hunk.old_line + hunk.old_range + 1,
|
|
new_line = target_line,
|
|
in_hunk = true,
|
|
}
|
|
-- target line is after the current hunk
|
|
else
|
|
current_new_line = hunk.new_line + hunk.new_range
|
|
current_old_line = hunk.old_line + hunk.old_range
|
|
end
|
|
end
|
|
-- target line is after last hunk
|
|
return {
|
|
old_line = current_old_line + (target_line - current_new_line),
|
|
new_line = target_line,
|
|
in_hunk = false,
|
|
}
|
|
else
|
|
for _, hunk in ipairs(hunks) do
|
|
-- target line is before current hunk
|
|
if target_line < hunk.old_line then
|
|
return {
|
|
old_line = target_line,
|
|
new_line = current_new_line + (target_line - current_old_line),
|
|
in_hunk = false,
|
|
}
|
|
-- target line is within the current hunk
|
|
elseif hunk.old_line <= target_line and target_line <= (hunk.old_line + hunk.old_range) then
|
|
return {
|
|
old_line = target_line,
|
|
new_line = hunk.new_line,
|
|
in_hunk = true,
|
|
}
|
|
-- target line is after the current hunk
|
|
else
|
|
current_new_line = hunk.new_line + hunk.new_range
|
|
current_old_line = hunk.old_line + hunk.old_range
|
|
end
|
|
end
|
|
-- target line is after last hunk
|
|
return {
|
|
old_line = current_old_line + (target_line - current_new_line),
|
|
new_line = target_line,
|
|
in_hunk = false,
|
|
}
|
|
end
|
|
end
|
|
|
|
---Check if current mode is visual mode
|
|
---@return boolean true if current mode is visual mode
|
|
M.check_visual_mode = function()
|
|
local mode = vim.api.nvim_get_mode().mode
|
|
if mode ~= "v" and mode ~= "V" then
|
|
vim.notify("Code suggestions are only available in visual mode", vim.log.levels.WARN)
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
---Return start line and end line of visual selection.
|
|
---Exists visual mode in order to access marks "<" , ">"
|
|
---@return integer,integer start line and end line
|
|
M.get_visual_selection_boundaries = function()
|
|
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", false, true, true), "nx", false)
|
|
local start_line = vim.api.nvim_buf_get_mark(0, "<")[1]
|
|
local end_line = vim.api.nvim_buf_get_mark(0, ">")[1]
|
|
return start_line, end_line
|
|
end
|
|
|
|
return M
|