initial commit

This commit is contained in:
Scott E. Graves 2022-07-29 09:46:02 -05:00
parent 57f0da4eca
commit 819a4620de
4 changed files with 644 additions and 0 deletions

0
doc/.keep Normal file
View File

582
lua/nvim-haven/init.lua Normal file
View File

@ -0,0 +1,582 @@
local action_state = require("telescope.actions.state")
local actions = require("telescope.actions")
local conf = require("telescope.config").values
local finders = require("telescope.finders")
local global_state = require("telescope.state")
local pfiletype = require("plenary.filetype")
local pickers = require("telescope.pickers")
local previewers = require("telescope.previewers")
local putils = require("telescope.previewers.utils")
local utils = require("nvim-haven.utils")
local ns_previewer = vim.api.nvim_create_namespace("telescope.previewers")
local M = {}
local active_saves = {}
local changed_lookup = {}
local directory_sep = utils.iff(utils.is_windows, "\\", "/")
local haven_config = {
enabled = true,
exclusions = {
function(path, _)
if utils.is_windows then
return path:lower():starts_with((vim.fn.eval("$VIMRUNTIME") .. directory_sep):lower())
end
return path:starts_with(vim.fn.eval("$VIMRUNTIME") .. directory_sep)
end,
function(path, _)
if utils.is_windows then
return path:lower():starts_with((vim.fn.stdpath("data") .. directory_sep):lower())
end
return path:starts_with(vim.fn.stdpath("data") .. directory_sep)
end,
function(path, _)
if utils.is_windows then
return path:lower():starts_with(
(utils.create_path(vim.fn.eval("$XDG_CONFIG_HOME"), "coc") .. directory_sep):lower()
)
end
return path:starts_with(
utils.create_path(vim.fn.eval("$XDG_CONFIG_HOME"), "coc") .. directory_sep
)
end,
function(path, _)
if utils.is_windows then
return path:lower():ends_with(
(directory_sep .. ".git" .. directory_sep .. "COMMIT_EDITMSG"):lower()
)
end
return path:ends_with(directory_sep .. ".git" .. directory_sep .. "COMMIT_EDITMSG")
end,
function(path, config)
if utils.is_windows then
return path:lower():starts_with((config.haven_path .. directory_sep):lower())
end
return path:starts_with(config.haven_path .. directory_sep)
end
},
haven_path = utils.create_path(vim.fn.stdpath("data"), "nvim-haven"),
inclusions = {},
max_history_count = 200,
save_timeout = 10000
}
local line_ending = utils.iff(utils.is_windows, "\r\n", "\n")
local print_message = function(...)
print("nvim-haven", ...)
end
local diff_strings = function(a, b)
return vim.diff(a, b, {algorithm = "minimal"})
end
local encode = function(str)
return str:gsub("\r?\n", "\r\n"):gsub(
"([^%w%-%.%_%~ ])",
function(c)
return string.format("%%%02X", string.byte(c))
end
):gsub(" ", "+")
end
local create_save_file = function(buf_info)
return utils.create_path(haven_config.haven_path, encode(buf_info.name) .. ".save")
end
local save_change_file = function(buf_info, lines, save_file)
print_message("save_changed_file", buf_info.name)
active_saves[save_file] = nil
local file, err = io.open(save_file, "a")
if file == nil then
print_message(err)
return
end
local file_entry =
vim.json.encode(
{
date = os.time(),
ft = pfiletype.detect(buf_info.name, {}),
lines = lines
}
)
_, err = file:write(file_entry .. line_ending)
if err ~= nil then
print_message(err)
end
file:close()
end
local save_change_file_entries = function(buf_info, entries, save_file)
print_message("save_changed_file", buf_info.name)
active_saves[save_file] = nil
local file, err = io.open(save_file, "w+")
if file == nil then
print_message(err)
return
end
for _, entry in pairs(entries) do
_, err = file:write(vim.json.encode(entry) .. line_ending)
if err ~= nil then
print_message(err)
end
end
file:close()
end
local read_change_file = function(buf_info, save_file)
local file, err = io.open(save_file, "r")
if file == nil then
return nil, err
end
local save_data
save_data, err = file:read("a")
if err ~= nil then
return nil, err
end
file:close()
local entries = vim.json.decode("[" .. table.concat(save_data:split(line_ending), ",") .. "]")
if #entries > haven_config.max_history_count then
while #entries > haven_config.max_history_count do
table.remove(entries, 1)
end
save_change_file_entries(buf_info, entries, save_file)
end
return entries
end
local process_file_changed = function(buf_info)
local save_file = create_save_file(buf_info)
local changed_data = changed_lookup[save_file]
local immediate = vim.fn.filereadable(save_file) == 0
if
not immediate and
(changed_data == nil or (buf_info.changed == 0 and changed_data.changed == 0) or
buf_info.changedtick == changed_data.changedtick)
then
changed_lookup[save_file] = {changed = buf_info.changed, changedtick = buf_info.changedtick}
return
end
if active_saves[save_file] ~= nil then
active_saves[save_file].timer:stop()
active_saves[save_file] = nil
end
changed_lookup[save_file] = {changed = buf_info.changed, changedtick = buf_info.changedtick}
local lines = vim.api.nvim_buf_get_lines(buf_info.bufnr, 0, -1, true)
local entries, _ = read_change_file(buf_info, save_file)
if entries ~= nil then
if
diff_strings(
table.concat(entries[#entries].lines, line_ending),
table.concat(lines, line_ending)
):len() == 0
then
return
end
entries = nil
end
local saved = false
local do_save = function()
if not saved then
saved = true
save_change_file(buf_info, lines, save_file)
end
end
if immediate then
do_save()
else
active_saves[save_file] = {
timer = vim.defer_fn(do_save, haven_config.save_timeout),
do_save = do_save
}
end
end
local check_requirements = function()
if vim.o.modifiable ~= 0 and vim.o.buftype ~= "nofile" then
local buf_info = vim.fn.getbufinfo(vim.fn.bufname())
if buf_info ~= nil and #buf_info > 0 then
buf_info = buf_info[1]
if buf_info.name:len() ~= 0 and vim.fn.filereadable(buf_info.name) ~= 0 then
if changed_lookup[create_save_file(buf_info)] == nil then
for _, is_included in pairs(haven_config.inclusions) do
if is_included(buf_info.name, haven_config) then
return true, buf_info
end
end
for _, is_excluded in pairs(haven_config.exclusions) do
if is_excluded(buf_info.name, haven_config) then
return false
end
end
end
return true, buf_info
end
end
end
return false
end
local handle_buffer_changed = function()
local ok, buf_info = check_requirements()
if ok and buf_info ~= nil then
process_file_changed(buf_info)
end
end
local handle_vim_leave = function()
for _, active in pairs(active_saves) do
active.timer:stop()
active.do_save()
end
active_saves = {}
changed_lookup = {}
end
local setup_autocmds = function()
local group_id = vim.api.nvim_create_augroup("nvim-haven-internal", {clear = true})
if haven_config.enabled then
vim.api.nvim_create_autocmd(
"BufEnter",
{
group = group_id,
pattern = "*",
callback = handle_buffer_changed
}
)
vim.api.nvim_create_autocmd(
"InsertLeave",
{
group = group_id,
pattern = "*",
callback = handle_buffer_changed
}
)
vim.api.nvim_create_autocmd(
"TextChanged",
{
group = group_id,
pattern = "*",
callback = handle_buffer_changed
}
)
vim.api.nvim_create_autocmd(
"VimLeave",
{
group = group_id,
pattern = "*",
callback = handle_vim_leave
}
)
else
vim.api.nvim_del_augroup_by_id(group_id)
end
end
local apply_diff_to_lines = function(diff, source_lines)
local diff_lines = diff:split(line_ending)
local changes = {}
local current_diff
for _, line in pairs(diff_lines) do
if line:len() > 0 then
if line:starts_with("@@") and line:ends_with("@@") then
local diff_range = line:sub(3, -1):sub(1, -3):split(" ")[1]:split(",")
if #diff_range == 1 then
table.insert(diff_range, "1")
end
local diff_start = math.abs(tonumber(diff_range[1], 10))
local diff_count = tonumber(diff_range[2], 10)
if diff_count == 0 then
diff_start = diff_start + 1
end
current_diff = {
diff = {line},
next = diff_start + diff_count,
start = diff_start
}
table.insert(changes, current_diff)
elseif current_diff ~= nil then
table.insert(current_diff.diff, line)
end
else
current_diff = nil
end
end
local actual_line = 1
local buffer_lines = {}
local diff_rows = {}
local source_line = 1
for _, change in pairs(changes) do
while source_line < change.start do
table.insert(buffer_lines, source_lines[source_line])
actual_line = actual_line + 1
source_line = source_line + 1
end
table.insert(diff_rows, actual_line)
for _, change_diff_lines in pairs(change.diff) do
table.insert(buffer_lines, change_diff_lines)
actual_line = actual_line + 1
end
source_line = change.next
end
while source_line <= #source_lines do
table.insert(buffer_lines, source_lines[source_line])
actual_line = actual_line + 1
source_line = source_line + 1
end
return buffer_lines, diff_rows
end
local show_picker = function(entries)
global_state.set_global_key("selected_entry", nil)
local jump_state
local jump_to_line = function(self, bufnr, lnum)
pcall(vim.api.nvim_buf_clear_namespace, bufnr, ns_previewer, 0, -1)
if lnum and lnum > 0 then
pcall(
vim.api.nvim_buf_add_highlight,
bufnr,
ns_previewer,
"TelescopePreviewLine",
lnum - 1,
0,
-1
)
pcall(vim.api.nvim_win_set_cursor, self.state.winid, {lnum, 0})
vim.api.nvim_buf_call(
bufnr,
function()
vim.cmd "norm! zz"
end
)
end
end
local do_forward_jump = function()
if jump_state ~= nil and #jump_state.diff_rows > 0 then
jump_state.cur =
utils.iff(
(jump_state.cur + 1) <= #jump_state.diff_rows,
jump_state.cur + 1,
#jump_state.diff_rows
)
jump_to_line(
jump_state.self,
jump_state.self.state.bufnr,
jump_state.diff_rows[jump_state.cur]
)
end
end
local do_reverse_jump = function()
if jump_state ~= nil and #jump_state.diff_rows > 0 then
jump_state.cur = utils.iff((jump_state.cur - 1) > 0, jump_state.cur - 1, 1)
jump_to_line(
jump_state.self,
jump_state.self.state.bufnr,
jump_state.diff_rows[jump_state.cur]
)
end
end
pickers.new(
{},
{
prompt_title = "File History",
previewer = previewers.new_buffer_previewer(
{
define_preview = function(self, entry)
jump_state = nil
if entry.index < #entries then
local previous_lines = entries[entry.index + 1].lines
local buffer_lines, diff_rows =
apply_diff_to_lines(
diff_strings(
table.concat(previous_lines, line_ending),
table.concat(entry.value.lines, line_ending)
),
previous_lines
)
previous_lines = nil
vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, buffer_lines)
putils.regex_highlighter(self.state.bufnr, "diff")
jump_state = {self = self, cur = 0, diff_rows = diff_rows}
vim.schedule(
function()
do_forward_jump()
end
)
else
vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, entry.value.lines)
putils.highlighter(self.state.bufnr, entry.value.ft, {})
end
end
}
),
sorter = conf.generic_sorter({}),
finder = finders.new_table(
{
results = entries,
entry_maker = function(item)
return {
value = item,
ordinal = tostring(item.date),
display = os.date("%m-%d-%Y %H:%M:%S", item.date)
}
end
}
),
attach_mappings = function(prompt_bufnr, map)
actions.select_default:replace(
function()
actions.close(prompt_bufnr)
local selection = action_state.get_selected_entry()
if selection ~= nil then
vim.api.nvim_buf_set_lines(0, 0, -1, false, selection.value.lines)
end
end
)
map("i", "<c-l>", do_forward_jump)
map("n", "<c-l>", do_forward_jump)
map("i", "<c-h>", do_reverse_jump)
map("n", "<c-h>", do_reverse_jump)
return true
end
}
):find()
end
M.setup = function(config)
if config == nil then
config = {}
end
if config.exclusions ~= nil then
for _, e in pairs(config.exclusions) do
if type(e) ~= "function" then
print_message(
"'exlcusions' contains an entry that is not a function. Skipping all exclusions until this is corrected:"
)
utils.dump_table(e)
break
end
table.insert(haven_config.exclusions, e)
end
end
haven_config.enabled = vim.F.if_nil(config.enabled, haven_config.enabled)
haven_config.haven_path = vim.F.if_nil(config.haven_path, haven_config.haven_path)
if config.inclusions ~= nil then
for _, e in pairs(config.inclusions) do
if type(e) ~= "function" then
print_message(
"'inclusions' contains an entry that is not a function. Skipping this inclusion until it is corrected:"
)
utils.dump_table(e)
end
table.insert(haven_config.inclusions, e)
end
end
haven_config.max_history_count =
vim.F.if_nil(config.max_history_count, haven_config.max_history_count)
if haven_config.max_history_count < 10 then
print_message("'max_history_count' too low: " .. haven_config.max_history_count)
haven_config.max_history_count = 100
print_message("reset 'max_history_count': " .. haven_config.max_history_count)
elseif haven_config.max_history_count > 500 then
print_message("'max_history_count' too high: " .. haven_config.max_history_count)
haven_config.max_history_count = 500
print_message("reset 'max_history_count': " .. haven_config.max_history_count)
end
haven_config.save_timeout = vim.F.if_nil(config.save_timeout, haven_config.save_timeout)
if haven_config.save_timeout < 135 then
print_message("'save_timeout' too low: " .. haven_config.save_timeout)
haven_config.save_timeout = 135
print_message("reset 'save_timeout': " .. haven_config.save_timeout)
elseif haven_config.save_timeout > 10000 then
print_message("'save_timeout' too high: " .. haven_config.save_timeout)
haven_config.save_timeout = 10000
print_message("reset 'save_timeout': " .. haven_config.save_timeout)
end
if vim.fn.mkdir(haven_config.haven_path, "p") == 0 then
print_message("directory create failed: " .. haven_config.haven_path)
haven_config.enabled = false
return
end
if vim.fn.isdirectory(haven_config.haven_path) == 0 then
print_message("directory not found: " .. haven_config.haven_path)
haven_config.enabled = false
return
end
setup_autocmds()
end
M.disable = function()
if haven_config.enabled then
haven_config.enabled = false
setup_autocmds()
end
end
M.enable = function()
if not haven_config.enabled then
haven_config.enabled = true
handle_buffer_changed()
setup_autocmds()
end
end
M.history = function(bufname)
bufname = vim.F.if_nil(bufname, vim.fn.bufname())
local buf_info = vim.fn.getbufinfo(bufname)
if buf_info ~= nil and #buf_info > 0 then
buf_info = buf_info[1]
local save_file = create_save_file(buf_info)
if vim.fn.filereadable(save_file) ~= 0 then
local entries, err = read_change_file(buf_info, save_file)
if entries == nil then
print_message(err)
return
end
show_picker(table.reverse(entries))
end
end
end
_G.Nvim_Haven_Disable = M.disable
_G.Nvim_Haven_Enable = M.enable
_G.Nvim_Haven_History = M.history
return M

View File

@ -0,0 +1,58 @@
local M = {}
function string:ends_with(suffix)
return suffix == "" or self:sub(-(#suffix)) == suffix
end
function string:split(sep)
sep = sep or ":"
local fields = {}
local pattern = string.format("([^%s]+)", sep)
_ =
self:gsub(
pattern,
function(c)
fields[#fields + 1] = c
end
)
return fields
end
function string:starts_with(prefix)
return self:sub(1, #prefix) == prefix
end
function table.reverse(self)
local n = #self
local i = 1
while i < n do
self[i], self[n] = self[n], self[i]
i = i + 1
n = n - 1
end
return self
end
function M.iff(b, l, r)
if b then
return l
end
return r
end
M.is_windows = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1
M.directory_sep = M.iff(M.is_windows, "\\\\", "/")
M.not_directory_sep = M.iff(M.is_windows, "/", "\\\\")
function M.create_path(...)
local Path = require "plenary.path"
local ret =
Path:new({...}):absolute():gsub(M.not_directory_sep, M.directory_sep):gsub(
M.directory_sep .. M.directory_sep,
M.directory_sep
)
return ret
end
return M

4
plugin/nvim-haven.lua Normal file
View File

@ -0,0 +1,4 @@
if vim.g.loaded_nvim_haven == 1 then
return
end
vim.g.loaded_nvim_haven = 1