Embed the Carbon.

This commit is contained in:
Andrey Parhomenko 2023-08-28 15:27:12 +03:00
parent 98587f4144
commit 6d861ae5f2
9 changed files with 2081 additions and 2 deletions

View file

@ -15,8 +15,7 @@ require("maps")
require("bootstrap") require("bootstrap")
require("dep") { require("dep") {
require("plugin.carbon")
} }
require("carbon").setup() require("carbon").setup({})

View file

@ -0,0 +1,6 @@
return {
hl = vim.api.nvim_create_namespace('carbon'),
hl_tmp = vim.api.nvim_create_namespace('carbon:tmp'),
augroup = vim.api.nvim_create_augroup('carbon', { clear = false }),
directions = { left = 'h', right = 'l', up = 'k', down = 'j' },
}

180
nvim/lua/carbon/entry.lua Normal file
View file

@ -0,0 +1,180 @@
local util = require('carbon.util')
local watcher = require('carbon.watcher')
local entry = {}
entry.items = {}
entry.__index = entry
entry.__lt = function(a, b)
if a.is_directory and b.is_directory then
return string.lower(a.name) < string.lower(b.name)
elseif a.is_directory then
return true
elseif b.is_directory then
return false
end
return string.lower(a.name) < string.lower(b.name)
end
function entry.new(path, parent)
local raw_path = path == '' and '/' or path
local clean = string.gsub(raw_path, '/+$', '')
local lstat = select(2, pcall(vim.loop.fs_lstat, raw_path)) or {}
local is_executable = lstat.mode == 33261
local is_directory = lstat.type == 'directory'
local is_symlink = lstat.type == 'link' and 1
if is_symlink then
local stat = select(2, pcall(vim.loop.fs_stat, raw_path))
if stat then
is_executable = lstat.mode == 33261
is_directory = stat.type == 'directory'
is_symlink = 1
else
is_symlink = 2
end
end
return setmetatable({
raw_path = raw_path,
path = clean,
name = vim.fn.fnamemodify(clean, ':t'),
parent = parent,
is_directory = is_directory,
is_executable = is_executable,
is_symlink = is_symlink,
}, entry)
end
function entry.find(path)
for _, children in pairs(entry.items) do
for _, child in ipairs(children) do
if child.path == path then
return child
end
end
end
end
function entry:synchronize(paths)
if not self.is_directory then
return
end
paths = paths or {}
if paths[self.path] then
paths[self.path] = nil
local all_paths = {}
local current_paths = {}
local previous_paths = {}
local previous_children = entry.items[self.path] or {}
self:set_children(nil)
for _, previous in ipairs(previous_children) do
all_paths[previous.path] = true
previous_paths[previous.path] = previous
end
for _, current in ipairs(self:children()) do
all_paths[current.path] = true
current_paths[current.path] = current
end
for path in pairs(all_paths) do
local current = current_paths[path]
local previous = previous_paths[path]
if previous and current then
if current.is_directory then
current:synchronize(paths)
end
elseif previous then
previous:terminate()
end
end
elseif self:has_children() then
for _, child in ipairs(self:children()) do
if child.is_directory then
child:synchronize(paths)
end
end
end
end
function entry:terminate()
watcher.release(self.path)
if self:has_children() then
for _, child in ipairs(self:children()) do
child:terminate()
end
self:set_children(nil)
end
if self.parent and self.parent:has_children() then
self.parent:set_children(vim.tbl_filter(function(sibling)
return sibling.path ~= self.path
end, entry.items[self.parent.path]))
end
end
function entry:children()
if self.is_directory and not self:has_children() then
self:set_children(self:get_children())
end
return entry.items[self.path] or {}
end
function entry:has_children()
return entry.items[self.path] and true or false
end
function entry:set_children(children)
entry.items[self.path] = children
end
function entry:get_children()
local entries = {}
local handle = vim.loop.fs_scandir(self.raw_path)
if type(handle) == 'userdata' then
local function iterator()
return vim.loop.fs_scandir_next(handle)
end
for name in iterator do
local absolute_path = self.path .. '/' .. name
local relative_path = vim.fn.fnamemodify(absolute_path, ':.')
if not util.is_excluded(relative_path) then
entries[#entries + 1] = entry.new(absolute_path, self)
end
end
table.sort(entries)
end
return entries
end
function entry:highlight_group()
if self.is_symlink == 1 then
return 'CarbonSymlink'
elseif self.is_symlink == 2 then
return 'CarbonBrokenSymlink'
elseif self.is_directory then
return 'CarbonDir'
elseif self.is_executable then
return 'CarbonExe'
else
return 'CarbonFile'
end
end
return entry

View file

@ -0,0 +1,79 @@
local util = require('carbon.util')
local view = require('carbon.view')
local watcher = require('carbon.watcher')
local health = {}
local function sort_names(a, b)
return string.lower(a) < string.lower(b)
end
local function sort_paths(a, b)
local a_is_directory = util.is_directory(a)
local b_is_directory = util.is_directory(b)
if a_is_directory and b_is_directory then
return sort_names(a, b)
elseif a_is_directory then
return true
elseif b_is_directory then
return false
end
return sort_names(a, b)
end
function health.check()
health.report_views()
health.report_listeners()
health.report_events()
end
function health.report_views()
vim.health.report_start('view::active')
local view_roots = vim.tbl_map(function(item)
return item.root
end, view.items)
table.sort(view_roots)
for _, root in ipairs(view_roots) do
vim.health.report_info(root.path)
end
end
function health.report_events()
vim.health.report_start('watcher::events')
local names = vim.tbl_keys(watcher.events)
table.sort(names, sort_names)
for _, name in ipairs(names) do
local callback_count = #vim.tbl_keys(watcher.events[name] or {})
local reporter = callback_count == 0 and 'report_warn' or 'report_info'
vim.health[reporter](
string.format(
'%d %s attached to %s',
callback_count,
callback_count == 1 and 'handler' or 'handlers',
name
)
)
end
end
function health.report_listeners()
vim.health.report_start('watcher::listeners')
local paths = vim.tbl_keys(watcher.listeners)
table.sort(paths, sort_paths)
for _, path in ipairs(paths) do
vim.health.report_info(path)
end
end
return health

378
nvim/lua/carbon/init.lua Normal file
View file

@ -0,0 +1,378 @@
local util = require('carbon.util')
local watcher = require('carbon.watcher')
local settings = require('carbon.settings')
local view = require('carbon.view')
local carbon = {}
function carbon.setup(user_settings)
if type(user_settings) ~= 'table' then
user_settings = {}
end
if not vim.g.carbon_initialized then
if type(user_settings) == 'function' then
user_settings(settings)
elseif type(user_settings) == 'table' then
local next = vim.tbl_deep_extend('force', settings, user_settings)
for setting, value in pairs(next) do
settings[setting] = value
end
end
if type(user_settings.highlights) == 'table' then
settings.highlights =
vim.tbl_extend('force', settings.highlights, user_settings.highlights)
end
local argv = vim.fn.argv()
local open = argv[1] and vim.fn.fnamemodify(argv[1], ':p') or vim.loop.cwd()
local command_opts = { bang = true, nargs = '?', complete = 'dir' }
watcher.on('carbon:synchronize', function(_, path)
view.resync(path)
end)
util.command('Carbon', carbon.explore, command_opts)
util.command('Rcarbon', carbon.explore_right, command_opts)
util.command('Lcarbon', carbon.explore_left, command_opts)
util.command('Fcarbon', carbon.explore_float, command_opts)
util.command('ToggleSidebarCarbon', carbon.toggle_sidebar, command_opts)
util.autocmd('SessionLoadPost', carbon.session_load_post, { pattern = '*' })
util.autocmd('WinResized', carbon.win_resized, { pattern = '*' })
if settings.open_on_dir then
util.autocmd('BufWinEnter', carbon.explore_buf_dir, { pattern = '*' })
end
--if settings.sync_on_cd then
util.autocmd('DirChanged', carbon.cd, { pattern = 'global' })
--end
if not settings.keep_netrw then
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1
pcall(vim.api.nvim_del_augroup_by_name, 'FileExplorer')
pcall(vim.api.nvim_del_augroup_by_name, 'Network')
util.command('Explore', carbon.explore, command_opts)
util.command('Rexplore', carbon.explore_right, command_opts)
util.command('Lexplore', carbon.explore_left, command_opts)
util.command('ToggleSidebarExplore', carbon.toggle_sidebar, command_opts)
end
for action in pairs(settings.defaults.actions) do
vim.keymap.set('', util.plug(action), carbon[action])
end
if type(settings.highlights) == 'table' then
for group, properties in pairs(settings.highlights) do
util.highlight(group, properties)
end
end
--[[
if
vim.fn.has('vim_starting')
and settings.auto_open
and util.is_directory(open)
then
view.activate({ path = open })
end
--]]
vim.g.carbon_initialized = true
end
end
function carbon.win_resized()
if vim.api.nvim_win_is_valid(view.sidebar.origin) then
local window_width = vim.api.nvim_win_get_width(view.sidebar.origin)
if window_width ~= settings.sidebar_width then
vim.api.nvim_win_set_width(view.sidebar.origin, settings.sidebar_width)
end
end
end
function carbon.session_load_post(event)
if util.is_directory(event.file) then
local window_id = util.bufwinid(event.buf)
local window_width = vim.api.nvim_win_get_width(window_id)
local is_sidebar = window_width == settings.sidebar_width
view.activate({ path = event.file })
view.execute(function(ctx)
ctx.view:show()
end)
if is_sidebar then
local neighbor = util.tbl_find(
util.window_neighbors(window_id, { 'left', 'right' }),
function(neighbor)
return neighbor.target
end
)
if neighbor then
view.sidebar = neighbor
end
end
end
end
function carbon.toggle_recursive()
view.execute(function(ctx)
if ctx.cursor.line.entry.is_directory then
local function toggle_recursive(target, value)
if target.is_directory then
ctx.view:set_path_attr(target.path, 'open', value)
if target:has_children() then
for _, child in ipairs(target:children()) do
toggle_recursive(child, value)
end
end
end
end
toggle_recursive(
ctx.cursor.line.entry,
not ctx.view:get_path_attr(ctx.cursor.line.entry.path, 'open')
)
ctx.view:update()
ctx.view:render()
end
end)
end
function carbon.edit()
view.execute(function(ctx)
if ctx.cursor.line.entry.is_directory then
local open = ctx.view:get_path_attr(ctx.cursor.line.entry.path, 'open')
ctx.view:set_path_attr(ctx.cursor.line.entry.path, 'open', not open)
ctx.view:update()
ctx.view:render()
else
view.handle_sidebar_or_float()
vim.cmd.edit({
ctx.cursor.line.entry.path,
mods = { keepalt = #vim.fn.getreg('#') ~= 0 },
})
end
end)
end
function carbon.split()
view.execute(function(ctx)
if not ctx.cursor.line.entry.is_directory then
if vim.w.carbon_fexplore_window then
vim.api.nvim_win_close(0, 1)
end
view.handle_sidebar_or_float()
vim.cmd.split(ctx.cursor.line.entry.path)
end
end)
end
function carbon.vsplit()
view.execute(function(ctx)
if not ctx.cursor.line.entry.is_directory then
if vim.w.carbon_fexplore_window then
vim.api.nvim_win_close(0, 1)
end
view.handle_sidebar_or_float()
vim.cmd.vsplit(ctx.cursor.line.entry.path)
end
end)
end
function carbon.up()
view.execute(function(ctx)
if ctx.view:up() then
ctx.view:update()
ctx.view:render()
util.cursor(1, 1)
end
end)
end
function carbon.reset()
view.execute(function(ctx)
if ctx.view:reset() then
ctx.view:update()
ctx.view:render()
util.cursor(1, 1)
end
end)
end
function carbon.down()
view.execute(function(ctx)
if ctx.view:down() then
ctx.view:update()
ctx.view:render()
util.cursor(1, 1)
end
end)
end
function carbon.cd(path)
view.execute(function(ctx)
local destination = path and path.file or path or vim.v.event.cwd
if ctx.view:cd(destination) then
ctx.view:update()
ctx.view:render()
util.cursor(1, 1)
end
end)
end
function carbon.explore(options_param)
local options = options_param or {}
local path =
util.explore_path(options.fargs and options.fargs[1] or '', view.current())
view.activate({ path = path, reveal = options.bang })
end
function carbon.toggle_sidebar(options)
local current_win = vim.api.nvim_get_current_win()
if vim.api.nvim_win_is_valid(view.sidebar.origin) then
vim.api.nvim_win_close(view.sidebar.origin, 1)
else
local explore_options = vim.tbl_extend(
'force',
options or {},
{ sidebar = settings.sidebar_position }
)
carbon.explore_sidebar(explore_options)
if not settings.sidebar_toggle_focus then
vim.api.nvim_set_current_win(current_win)
end
end
end
function carbon.explore_sidebar(options_param)
local options = options_param or {}
local sidebar = options.sidebar or settings.sidebar_position
local path =
util.explore_path(options.fargs and options.fargs[1] or '', view.current())
view.activate({ path = path, reveal = options.bang, sidebar = sidebar })
end
function carbon.explore_left(options_param)
if view.sidebar.position ~= 'left' then
view.close_sidebar()
end
carbon.explore_sidebar(
vim.tbl_extend('force', options_param or {}, { sidebar = 'left' })
)
end
function carbon.explore_right(options_param)
if view.sidebar.position ~= 'right' then
view.close_sidebar()
end
carbon.explore_sidebar(
vim.tbl_extend('force', options_param or {}, { sidebar = 'right' })
)
end
function carbon.explore_float(options_param)
local options = options_param or {}
local path =
util.explore_path(options.fargs and options.fargs[1] or '', view.current())
view.activate({ path = path, reveal = options.bang, float = true })
end
function carbon.explore_buf_dir(params)
if vim.bo.filetype == 'carbon.explorer' then
return
end
if params and params.file and util.is_directory(params.file) then
view.activate({ path = params.file })
view.execute(function(ctx)
ctx.view:show()
end)
end
end
function carbon.quit()
if #vim.api.nvim_list_wins() > 1 then
vim.api.nvim_win_close(0, 1)
elseif #vim.api.nvim_list_bufs() > 1 then
pcall(vim.cmd.bprevious)
end
end
function carbon.create()
view.execute(function(ctx)
ctx.view:create()
end)
end
function carbon.delete()
view.execute(function(ctx)
ctx.view:delete()
end)
end
function carbon.move()
view.execute(function(ctx)
ctx.view:move()
end)
end
function carbon.close_parent()
view.execute(function(ctx)
local count = 0
local lines = { unpack(ctx.view:current_lines(), 2) }
local entry = ctx.cursor.line.entry
local line
while count < vim.v.count1 do
line = util.tbl_find(lines, function(current)
return current.entry == entry.parent
or vim.tbl_contains(current.path, entry.parent)
end)
if line then
count = count + 1
entry = line.path[1] and line.path[1].parent or line.entry
ctx.view:set_path_attr(entry.path, 'open', false)
else
break
end
end
line = util.tbl_find(lines, function(current)
return current.entry == entry or vim.tbl_contains(current.path, entry)
end)
if line then
vim.fn.cursor(line.lnum, (line.depth + 1) * 2 + 1)
end
ctx.view:update()
ctx.view:render()
end)
end
return carbon

View file

@ -0,0 +1,82 @@
local defaults = {
sync_pwd = false,
compress = true,
auto_open = true,
keep_netrw = false,
file_icons = pcall(require, 'nvim-web-devicons'),
sync_on_cd = not vim.opt.autochdir:get(),
sync_delay = 20,
open_on_dir = true,
auto_reveal = false,
sidebar_width = 30,
sidebar_toggle_focus = true,
sidebar_position = 'left',
exclude = {
'~$',
'#$',
'%.git$',
'%.bak$',
'%.rbc$',
'%.class$',
'%.sw[a-p]$',
'%.py[cod]$',
'%.Trashes$',
'%.DS_Store$',
'Thumbs%.db$',
'__pycache__',
'node_modules',
},
indicators = {
expand = '+',
collapse = '-',
},
flash = {
delay = 50,
duration = 500,
},
float_settings = function()
local columns = vim.opt.columns:get()
local rows = vim.opt.lines:get()
local width = math.min(40, math.floor(columns * 0.9))
local height = math.min(20, math.floor(rows * 0.9))
return {
relative = 'editor',
style = 'minimal',
border = 'rounded',
width = width,
height = height,
col = math.floor(columns / 2 - width / 2),
row = math.floor(rows / 2 - height / 2 - 2),
}
end,
actions = {
up = '[',
down = ']',
quit = 'q',
edit = '<cr>',
move = 'm',
reset = 'u',
split = { '<c-x>', '<c-s>' },
vsplit = '<c-v>',
create = { 'c', '%' },
delete = 'd',
close_parent = '-',
toggle_recursive = '!',
},
highlights = {
CarbonDir = { link = 'Directory' },
CarbonFile = { link = 'Text' },
CarbonExe = { link = '@function.builtin' },
CarbonSymlink = { link = '@include' },
CarbonBrokenSymlink = { link = 'DiagnosticError' },
CarbonIndicator = { fg = 'Gray', ctermfg = 'DarkGray', bold = true },
CarbonFloat = { bg = '#111111', ctermbg = 'black' },
CarbonFloatBorder = { link = 'CarbonFloat' },
CarbonDanger = { link = 'Error' },
CarbonPending = { link = 'Search' },
CarbonFlash = { link = 'Visual' },
},
}
return vim.tbl_extend('force', vim.deepcopy(defaults), { defaults = defaults })

323
nvim/lua/carbon/util.lua Normal file
View file

@ -0,0 +1,323 @@
local constants = require('carbon.constants')
local settings = require('carbon.settings')
local util = {}
function util.explore_path(path, current_view)
path = string.gsub(path, '%s', '')
if path == '' then
path = vim.loop.cwd()
end
if not vim.startswith(path, '/') then
local base_path = current_view and current_view.root.path or vim.loop.cwd()
path = string.format('%s/%s', base_path, path)
end
return string.gsub(vim.fn.simplify(path), '/+$', '')
end
function util.resolve(path)
return string.gsub(
vim.fn.fnamemodify(vim.fs.normalize(path), ':p'),
'/+$',
''
)
end
function util.is_excluded(path)
if settings.exclude then
for _, pattern in ipairs(settings.exclude) do
if string.find(path, pattern) then
return true
end
end
end
return false
end
function util.cursor(row, col)
return vim.api.nvim_win_set_cursor(0, { row, col - 1 })
end
function util.is_directory(path)
return (vim.loop.fs_stat(path) or {}).type == 'directory'
end
function util.plug(name)
return string.format('<plug>(carbon-%s)', string.gsub(name, '_', '-'))
end
function util.tbl_key(tbl, item)
for key, tbl_item in pairs(tbl) do
if tbl_item == item then
return key
end
end
end
function util.tbl_some(tbl, callback)
for key, value in pairs(tbl) do
if callback(value, key) then
return true
end
end
return false
end
function util.tbl_find(tbl, callback)
for key, value in pairs(tbl) do
if callback(value, key) then
return value, key
end
end
end
function util.tbl_except(tbl, keys)
local result = {}
for key, value in pairs(tbl) do
if not vim.tbl_contains(keys, key) then
result[key] = value
end
end
return result
end
function util.autocmd(event, cmd_or_callback, opts)
return vim.api.nvim_create_autocmd(
event,
vim.tbl_extend('force', {
group = constants.augroup,
callback = cmd_or_callback,
}, opts or {})
)
end
function util.clear_autocmd(event, opts)
return vim.api.nvim_clear_autocmds(vim.tbl_extend('force', {
group = constants.augroup,
event = event,
}, opts or {}))
end
function util.command(lhs, rhs, options)
return vim.api.nvim_create_user_command(lhs, rhs, options or {})
end
function util.highlight(group, opts)
local merged = vim.tbl_extend('force', { default = true }, opts or {})
vim.api.nvim_set_hl(0, group, merged)
end
function util.confirm(options)
local finished = false
local actions = {}
local mappings = {}
local lines = {}
local function finish(label, immediate)
local function handler()
if finished then
return nil
end
finished = true
local callback = actions[label] and actions[label].callback
if type(callback) == 'function' then
callback()
end
vim.cmd.close()
end
if not immediate then
return handler
end
handler()
end
for ascii = 32, 127 do
if
ascii < 48
and ascii > 57
and not vim.tbl_contains({ 38, 40, 74, 75, 106, 107 }, ascii)
then
mappings[#mappings + 1] = { 'n', string.char(ascii), '<nop>' }
end
end
for _, action in ipairs(options.actions) do
actions[action.label] = action
if action.shortcut then
mappings[#mappings + 1] = { 'n', action.shortcut, finish(action.label) }
lines[#lines + 1] = ' [' .. action.shortcut .. '] ' .. action.label
else
lines[#lines + 1] = ' ' .. action.label
end
end
mappings[#mappings + 1] = { 'n', '<esc>', finish('cancel') }
mappings[#mappings + 1] = {
'n',
'<cr>',
function()
finish(string.sub(vim.fn.getline('.'), 6), true)
end,
}
local buf = util.create_scratch_buf({
modifiable = false,
lines = lines,
mappings = mappings,
autocmds = {
BufLeave = finish('cancel'),
CursorMoved = function()
util.cursor(vim.fn.line('.'), 3)
end,
},
})
local win = vim.api.nvim_open_win(buf, true, {
relative = 'win',
anchor = 'NW',
border = 'single',
style = 'minimal',
row = options.row or vim.fn.line('.'),
col = options.col or vim.fn.col('.'),
height = #lines,
width = 1 + math.max(unpack(vim.tbl_map(function(line)
return #line
end, lines))),
})
util.cursor(1, 3)
vim.api.nvim_win_set_option(win, 'cursorline', true)
util.set_winhl(win, {
Normal = options.highlight or 'CarbonIndicator',
FloatBorder = options.highlight or 'Normal',
CursorLine = options.highlight or 'Normal',
})
end
function util.bufwinid(buf)
for _, win in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_get_buf(win) == buf then
return win
end
end
end
function util.find_buf_by_name(name)
return util.tbl_find(vim.api.nvim_list_bufs(), function(bufnr)
return name == vim.api.nvim_buf_get_name(bufnr)
end)
end
function util.create_scratch_buf(options)
options = options or {}
local found = util.find_buf_by_name(options.name)
local buf = found or vim.api.nvim_create_buf(false, true)
local buffer_options = vim.tbl_extend('force', {
bufhidden = 'wipe',
buftype = 'nofile',
swapfile = false,
}, util.tbl_except(options, { 'name', 'lines', 'mappings', 'autocmds' }))
if options.name then
vim.api.nvim_buf_set_name(buf, options.name == '' and '/' or options.name)
end
if options.lines then
vim.api.nvim_buf_set_lines(buf, 0, -1, 1, options.lines)
vim.api.nvim_buf_set_option(buf, 'modified', false)
end
if options.mappings then
util.set_buf_mappings(buf, options.mappings)
end
if options.autocmds then
util.set_buf_autocmds(buf, options.autocmds)
end
for option, value in pairs(buffer_options) do
vim.api.nvim_buf_set_option(buf, option, value)
end
return buf
end
function util.set_buf_mappings(buf, mappings)
for _, mapping in ipairs(mappings) do
vim.keymap.set(
mapping[1],
mapping[2],
mapping[3],
vim.tbl_extend('force', mapping[4] or {}, { buffer = buf })
)
end
end
function util.set_buf_autocmds(buf, autocmds)
for autocmd, rhs in pairs(autocmds) do
util.autocmd(autocmd, rhs, { buffer = buf })
end
end
function util.set_winhl(win, highlights)
local winhls = {}
for source, target in pairs(highlights) do
winhls[#winhls + 1] = source .. ':' .. target
end
vim.api.nvim_win_set_option(win, 'winhl', table.concat(winhls, ','))
end
function util.clear_extmarks(buf, ...)
local extmarks = vim.api.nvim_buf_get_extmarks(buf, constants.hl, ...)
for _, extmark in ipairs(extmarks) do
vim.api.nvim_buf_del_extmark(buf, constants.hl, extmark[1])
end
end
function util.add_highlight(buf, ...)
vim.api.nvim_buf_add_highlight(buf, constants.hl, ...)
end
function util.window_neighbors(window_id, sides)
local original_window = vim.api.nvim_get_current_win()
local result = {}
for _, side in ipairs(sides or {}) do
vim.api.nvim_set_current_win(window_id)
vim.cmd.wincmd(constants.directions[side])
local side_id = vim.api.nvim_get_current_win()
local result_id = window_id ~= side_id and side_id or nil
if result_id then
result[#result + 1] = {
origin = window_id,
position = side,
target = result_id,
}
end
end
vim.api.nvim_set_current_win(original_window)
return result
end
return util

945
nvim/lua/carbon/view.lua Normal file
View file

@ -0,0 +1,945 @@
local util = require('carbon.util')
local entry = require('carbon.entry')
local watcher = require('carbon.watcher')
local settings = require('carbon.settings')
local constants = require('carbon.constants')
local view = {}
view.__index = view
view.sidebar = { origin = -1, target = -1 }
view.float = { origin = -1, target = -1 }
view.items = {}
view.resync_paths = {}
local function create_leave(ctx)
vim.cmd.stopinsert()
ctx.view:set_path_attr(ctx.target.path, 'compressible', ctx.prev_compressible)
util.cursor(ctx.target_line.lnum, 1)
vim.keymap.del('i', '<cr>', { buffer = 0 })
vim.keymap.del('i', '<esc>', { buffer = 0 })
util.clear_autocmd('CursorMovedI', { buffer = 0 })
ctx.view:update()
ctx.view:render()
end
local function create_confirm(ctx)
return function()
local text = vim.trim(string.sub(vim.fn.getline('.'), ctx.edit_col))
local name = vim.fn.fnamemodify(text, ':t')
local parent_directory = ctx.target.path
.. '/'
.. vim.trim(vim.fn.fnamemodify(text, ':h'))
vim.fn.mkdir(parent_directory, 'p')
if name ~= '' then
vim.fn.writefile({}, parent_directory .. '/' .. name)
end
create_leave(ctx)
view.resync(vim.fn.fnamemodify(parent_directory, ':h'))
end
end
local function create_cancel(ctx)
return function()
ctx.view:set_path_attr(ctx.target.path, 'open', ctx.prev_open)
create_leave(ctx)
end
end
local function create_insert_move(ctx)
return function()
local text = ctx.edit_prefix
.. vim.trim(string.sub(vim.fn.getline('.'), ctx.edit_col))
local last_slash_col = vim.fn.strridx(text, '/') + 1
vim.api.nvim_buf_set_lines(0, ctx.edit_lnum, ctx.edit_lnum + 1, 1, { text })
util.clear_extmarks(0, { ctx.edit_lnum, 0 }, { ctx.edit_lnum, -1 }, {})
util.add_highlight(0, 'CarbonDir', ctx.edit_lnum, 0, last_slash_col)
util.add_highlight(0, 'CarbonFile', ctx.edit_lnum, last_slash_col, -1)
util.cursor(ctx.edit_lnum + 1, math.max(ctx.edit_col, vim.fn.col('.')))
end
end
function view.file_icons()
if settings.file_icons then
local ok, module = pcall(require, 'nvim-web-devicons')
if ok then
return module
end
end
end
function view.find(path)
local resolved = util.resolve(path)
return util.tbl_find(view.items, function(target_view)
return target_view.root.path == resolved
end)
end
function view.get(path)
local found_view = view.find(path)
if found_view then
return found_view
end
local index = #view.items + 1
local resolved = util.resolve(path)
local instance = setmetatable({
index = index,
initial = resolved,
states = {},
root = entry.new(resolved),
}, view)
view.items[index] = instance
return instance
end
function view.activate(options_param)
local options = options_param or {}
local original_window = vim.api.nvim_get_current_win()
local current_view = (options.path and view.get(options.path))
or view.current()
or view.get(vim.loop.cwd())
if options.reveal or settings.auto_reveal then
current_view:expand_to_path(vim.fn.expand('%'))
end
if options.sidebar then
if vim.api.nvim_win_is_valid(view.sidebar.origin) then
vim.api.nvim_set_current_win(view.sidebar.origin)
else
local split = options.sidebar == 'right' and 'botright' or 'topleft'
local target_side = options.sidebar == 'right' and 'left' or 'right'
vim.cmd.split({ mods = { vertical = true, split = split } })
local origin_id = vim.api.nvim_get_current_win()
local neighbor = util.window_neighbors(origin_id, { target_side })[1]
local target = neighbor and neighbor.target or original_window
view.sidebar = {
position = options.sidebar,
origin = origin_id,
target = target,
}
end
vim.api.nvim_win_set_width(view.sidebar.origin, settings.sidebar_width)
vim.api.nvim_win_set_buf(view.sidebar.origin, current_view:buffer())
elseif options.float then
local float_settings = settings.float_settings
or settings.defaults.float_settings
float_settings = type(float_settings) == 'function' and float_settings()
or vim.deepcopy(float_settings)
view.float = {
origin = vim.api.nvim_open_win(current_view:buffer(), 1, float_settings),
target = original_window,
}
vim.api.nvim_win_set_option(
view.float.origin,
'winhl',
'FloatBorder:CarbonFloatBorder,Normal:CarbonFloat'
)
else
vim.api.nvim_win_set_buf(0, current_view:buffer())
end
end
function view.close_sidebar()
if vim.api.nvim_win_is_valid(view.sidebar.origin) then
vim.api.nvim_win_close(view.sidebar.origin, true)
end
view.sidebar = { origin = -1, target = -1 }
end
function view.close_float()
if vim.api.nvim_win_is_valid(view.float.origin) then
vim.api.nvim_win_close(view.float.origin, true)
end
view.float = { origin = -1, target = -1 }
end
function view.handle_sidebar_or_float()
local current_window = vim.api.nvim_get_current_win()
if current_window == view.sidebar.origin then
if vim.api.nvim_win_is_valid(view.sidebar.target) then
vim.api.nvim_set_current_win(view.sidebar.target)
else
local split = view.sidebar.position == 'right' and 'topleft' or 'botright'
local target_side = view.sidebar.position == 'right' and 'left' or 'right'
local neighbor =
util.window_neighbors(view.sidebar.origin, { target_side })[1]
if neighbor then
view.sidebar.target = neighbor.target
vim.api.nvim_set_current_win(neighbor.target)
else
vim.cmd.split({ mods = { vertical = true, split = split } })
view.sidebar.target = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_width(view.sidebar.origin, settings.sidebar_width)
end
end
elseif current_window == view.float.origin then
view.close_float()
end
end
function view.current()
local bufnr = vim.api.nvim_get_current_buf()
local ref = select(2, pcall(vim.api.nvim_buf_get_var, bufnr, 'carbon'))
return ref and view.items[ref.index] or false
end
function view.execute(callback)
local current_view = view.current()
if current_view then
return callback({ cursor = current_view:cursor(), view = current_view })
end
end
function view.resync(path)
view.resync_paths[path] = true
if view.resync_timer and not view.resync_timer:is_closing() then
view.resync_timer:close()
end
view.resync_timer = vim.defer_fn(function()
for _, current_view in ipairs(view.items) do
current_view.root:synchronize(view.resync_paths)
current_view:update()
current_view:render()
end
if not view.resync_timer:is_closing() then
view.resync_timer:close()
end
view.resync_timer = nil
view.resync_paths = {}
end, settings.sync_delay)
end
function view:expand_to_path(path)
local resolved = util.resolve(path)
if vim.startswith(resolved, self.root.path) then
local dirs = vim.split(string.sub(resolved, #self.root.path + 2), '/')
local current = self.root
for _, dir in ipairs(dirs) do
current:children()
current = entry.find(string.format('%s/%s', current.path, dir))
if current then
self:set_path_attr(current.path, 'open', true)
else
break
end
end
if current and current.path == resolved then
self.flash = current
self:update()
return true
end
return false
end
end
function view:get_path_attr(path, attr)
local state = self.states[path]
local value = state and state[attr]
if attr == 'compressible' and value == nil then
return true
end
return value
end
function view:set_path_attr(path, attr, value)
if not self.states[path] then
self.states[path] = {}
end
self.states[path][attr] = value
return value
end
function view:buffers()
return vim.tbl_filter(function(bufnr)
local ref = select(2, pcall(vim.api.nvim_buf_get_var, bufnr, 'carbon'))
return ref and ref.index == self.index
end, vim.api.nvim_list_bufs())
end
function view:update()
self.cached_lines = nil
end
function view:render()
local cursor
local lines = {}
local hls = {}
for lnum, line_data in ipairs(self:current_lines()) do
lines[#lines + 1] = line_data.line
if self.flash and self.flash.path == line_data.entry.path then
cursor = { lnum = lnum, col = 1 + (line_data.depth + 1) * 2 }
end
for _, hl in ipairs(line_data.highlights) do
hls[#hls + 1] = { hl[1], lnum - 1, hl[2], hl[3] }
end
end
local buf = self:buffer()
local current_mode = string.lower(vim.api.nvim_get_mode().mode)
vim.api.nvim_buf_clear_namespace(buf, constants.hl, 0, -1)
vim.api.nvim_buf_set_option(buf, 'modifiable', true)
vim.api.nvim_buf_set_lines(buf, 0, -1, 1, lines)
vim.api.nvim_buf_set_option(buf, 'modified', false)
if not string.find(current_mode, 'i') then
vim.api.nvim_buf_set_option(buf, 'modifiable', false)
end
for _, hl in ipairs(hls) do
vim.api.nvim_buf_add_highlight(
buf,
constants.hl,
hl[1],
hl[2],
hl[3],
hl[4]
)
end
if cursor then
util.cursor(cursor.lnum, cursor.col)
if settings.flash then
vim.defer_fn(function()
self:focus_flash(
settings.flash.duration,
'CarbonFlash',
{ cursor.lnum - 1, cursor.col - 1 },
{ cursor.lnum - 1, -1 }
)
end, settings.flash.delay)
end
end
self.flash = nil
end
function view:focus_flash(duration, group, start, finish)
local buf = self:buffer()
vim.highlight.range(buf, constants.hl_tmp, group, start, finish, {})
vim.defer_fn(function()
if vim.api.nvim_buf_is_valid(buf) then
vim.api.nvim_buf_clear_namespace(buf, constants.hl_tmp, 0, -1)
end
end, duration)
end
function view:buffer()
local buffers = self:buffers()
if buffers[1] then
return buffers[1]
end
local mappings = {
{ 'n', 'i', '<nop>' },
{ 'n', 'I', '<nop>' },
{ 'n', 'o', '<nop>' },
{ 'n', 'O', '<nop>' },
}
for action, mapping in pairs(settings.actions or {}) do
if type(mapping) == 'string' then
mapping = { mapping }
end
if type(mapping) == 'table' then
for _, key in ipairs(mapping) do
mappings[#mappings + 1] =
{ 'n', key, util.plug(action), { nowait = true } }
end
end
end
local buffer = util.create_scratch_buf({
name = self.root.path,
filetype = 'carbon.explorer',
modifiable = false,
modified = false,
bufhidden = 'wipe',
mappings = mappings,
autocmds = {
BufHidden = function()
self:hide()
end,
BufWinEnter = function()
self:show()
end,
},
})
vim.api.nvim_buf_set_var(
buffer,
'carbon',
{ index = self.index, path = self.root.path }
)
return buffer
end
function view:hide() -- luacheck:ignore unused argument self
vim.opt_local.wrap = vim.opt_global.wrap:get()
vim.opt_local.spell = vim.opt_global.spell:get()
vim.opt_local.fillchars = vim.opt_global.fillchars:get()
view.sidebar = { origin = -1, target = -1 }
view.float = { origin = -1, target = -1 }
end
function view:show()
vim.opt_local.wrap = false
vim.opt_local.spell = false
vim.opt_local.fillchars = { eob = ' ' }
self:render()
end
function view:up(count)
local parents = self:parents(count)
local destination = parents[#parents]
if destination and self:switch_to_existing_view(destination.path) then
return true
end
for idx, parent_entry in ipairs(parents) do
self:set_path_attr(self.root.path, 'open', true)
self:set_root(parent_entry, { rename = idx == #parents })
end
return #parents ~= 0
end
function view:reset()
return self:cd(self.initial)
end
function view:cd(path)
if path == self.root.path then
return false
elseif vim.startswith(self.root.path, path) then
local new_depth = select(2, string.gsub(path, '/', ''))
local current_depth = select(2, string.gsub(self.root.path, '/', ''))
if current_depth - new_depth > 0 then
return self:up(current_depth - new_depth)
end
elseif self:switch_to_existing_view(path) then
return true
else
return self:set_root(entry.find(path) or entry.new(path))
end
end
function view:down(count)
local cursor = self:cursor()
local new_root = cursor.line.path[count or vim.v.count1] or cursor.line.entry
if not new_root.is_directory then
new_root = new_root.parent
end
if not new_root or new_root.path == self.root.path then
return false
end
if self:switch_to_existing_view(new_root.path) then
return true
else
self:set_path_attr(self.root.path, 'open', true)
return self:set_root(new_root)
end
end
function view:set_root(target, options_param)
local options = options_param or {}
local is_cwd = self.root.path == vim.loop.cwd()
if type(target) == 'string' then
target = entry.new(target)
end
if target.path == self.root.path then
return false
end
self.root = target
if options.rename ~= false then
vim.api.nvim_buf_set_name(self:buffer(), self.root.raw_path)
end
vim.api.nvim_buf_set_var(
self:buffer(),
'carbon',
{ index = self.index, path = self.root.path }
)
watcher.keep(function(path)
return util.tbl_some(view.items, function(current_view)
return vim.startswith(path, current_view.root.path)
end)
end)
if settings.sync_pwd and is_cwd then
vim.api.nvim_set_current_dir(self.root.path)
end
return true
end
function view:current_lines()
if not self.cached_lines then
self.cached_lines = self:lines()
end
return self.cached_lines
end
function view:lines(input_target, lines, depth)
lines = lines or {}
depth = depth or 0
local target = input_target or self.root
local expand_indicator = ' '
local collapse_indicator = ' '
local file_icons = view.file_icons()
if type(settings.indicators) == 'table' then
expand_indicator = settings.indicators.expand or expand_indicator
collapse_indicator = settings.indicators.collapse or collapse_indicator
end
if not input_target and #lines == 0 then
lines[#lines + 1] = {
lnum = 1,
depth = -1,
entry = self.root,
line = self.root.name .. '/',
highlights = { { 'CarbonDir', 0, -1 } },
icon_width = 0,
path = {},
}
watcher.register(self.root.path)
end
for _, child in ipairs(target:children()) do
local tmp = child
local hls = {}
local path = {}
local lnum = 1 + #lines
local indent = string.rep(' ', depth)
local is_empty = true
local indicator = ''
local path_suffix = ''
if settings.compress then
while
tmp.is_directory
and #tmp:children() == 1
and self:get_path_attr(tmp.path, 'compressible')
do
watcher.register(tmp.path)
path[#path + 1] = tmp
tmp = tmp:children()[1]
end
end
if tmp.is_directory then
watcher.register(tmp.path)
is_empty = #tmp:children() == 0
path_suffix = '/'
if not is_empty and self:get_path_attr(tmp.path, 'open') then
indicator = collapse_indicator
elseif not is_empty then
indicator = expand_indicator
else
indent = indent .. ' '
end
else
indent = indent .. ' '
end
local icon = ''
local icon_highlight
if file_icons and settings.file_icons and not tmp.is_directory then
local info = {
file_icons.get_icon(
tmp.name .. path_suffix,
vim.fn.fnamemodify(tmp.name, ':e'),
{ default = true }
),
}
icon = info[1] or ' '
icon_highlight = info[2]
end
local full_path = tmp.name .. path_suffix
local indent_end = #indent
local icon_width = #icon ~= 0 and #icon + 1 or 0
local indicator_width = #indicator ~= 0 and #indicator + 1 or 0
local path_start = indent_end + icon_width + indicator_width
local dir_path = table.concat(
vim.tbl_map(function(parent)
return parent.name
end, path),
'/'
)
if path[1] then
full_path = dir_path .. '/' .. full_path
end
if indicator_width ~= 0 and not is_empty then
hls[#hls + 1] =
{ 'CarbonIndicator', indent_end, indent_end + indicator_width }
end
if icon and icon_highlight then
hls[#hls + 1] =
{ icon_highlight, indent_end + indicator_width, path_start - 1 }
end
local entries = { unpack(path) }
entries[#entries + 1] = tmp
for _, current_entry in ipairs(entries) do
local part = current_entry.name .. '/'
local path_end = path_start + #part
local highlight_group = 'CarbonFile'
if current_entry.is_symlink == 1 then
highlight_group = 'CarbonSymlink'
elseif current_entry.is_symlink == 2 then
highlight_group = 'CarbonBrokenSymlink'
elseif current_entry.is_directory then
highlight_group = 'CarbonDir'
elseif current_entry.is_executable then
highlight_group = 'CarbonExe'
end
hls[#hls + 1] = { highlight_group, path_start, path_end }
path_start = path_end
end
local line_prefix = indent
if indicator_width ~= 0 then
line_prefix = line_prefix .. indicator .. ' '
end
if icon_width ~= 0 then
line_prefix = line_prefix .. icon .. ' '
end
lines[#lines + 1] = {
lnum = lnum,
depth = depth,
entry = tmp,
line = line_prefix .. full_path,
icon_width = icon_width,
highlights = hls,
path = path,
}
if tmp.is_directory and self:get_path_attr(tmp.path, 'open') then
self:lines(tmp, lines, depth + 1)
end
end
return lines
end
function view:cursor(opts)
local options = opts or {}
local lines = self:current_lines()
local line = lines[vim.fn.line('.')]
local target = line.entry
local target_line
if options.target_directory_only and not target.is_directory then
target = target.parent
end
target = line.path[vim.v.count] or target
target_line = util.tbl_find(lines, function(current)
if current.entry.path == target.path then
return true
end
return util.tbl_find(current.path, function(parent)
if parent.path == target.path then
return true
end
end)
end)
return { line = line, target = target, target_line = target_line }
end
function view:create()
local ctx = self:cursor({ target_directory_only = true })
ctx.view = self
ctx.compact = ctx.target.is_directory and #ctx.target:children() == 0
ctx.prev_open = self:get_path_attr(ctx.target.path, 'open')
ctx.prev_compressible = self:get_path_attr(ctx.target.path, 'compressible')
self:set_path_attr(ctx.target.path, 'open', true)
self:set_path_attr(ctx.target.path, 'compressible', false)
if ctx.compact then
ctx.edit_prefix = ctx.line.line
ctx.edit_lnum = ctx.line.lnum - 1
ctx.edit_col = #ctx.edit_prefix + 1
ctx.init_end_lnum = ctx.edit_lnum + 1
else
ctx.edit_prefix = string.rep(' ', ctx.target_line.depth + 2)
ctx.edit_lnum = ctx.target_line.lnum + #self:lines(ctx.target)
ctx.edit_col = #ctx.edit_prefix + 1
ctx.init_end_lnum = ctx.edit_lnum
end
self:update()
self:render()
util.autocmd('CursorMovedI', create_insert_move(ctx), { buffer = 0 })
vim.keymap.set('i', '<cr>', create_confirm(ctx), { buffer = 0 })
vim.keymap.set('i', '<esc>', create_cancel(ctx), { buffer = 0 })
vim.cmd.startinsert({ bang = true })
vim.api.nvim_buf_set_option(0, 'modifiable', true)
vim.api.nvim_buf_set_lines(
0,
ctx.edit_lnum,
ctx.init_end_lnum,
1,
{ ctx.edit_prefix }
)
util.cursor(ctx.edit_lnum + 1, ctx.edit_col)
end
function view:delete()
local cursor = self:cursor()
local targets = vim.list_extend(
{ unpack(cursor.line.path) },
{ cursor.line.entry }
)
local lnum_idx = cursor.line.lnum - 1
local count = vim.v.count == 0 and #targets or vim.v.count1
local path_idx = math.min(count, #targets)
local target = targets[path_idx]
local highlight = {
'CarbonFile',
cursor.line.depth * 2 + 2 + cursor.line.icon_width,
lnum_idx,
}
if targets[path_idx].path == self.root.path then
return
end
if target.is_directory then
highlight[1] = 'CarbonDir'
end
for idx = 1, path_idx - 1 do
highlight[2] = highlight[2] + #cursor.line.path[idx].name + 1
end
util.clear_extmarks(0, { lnum_idx, highlight[2] }, { lnum_idx, -1 }, {})
util.add_highlight(0, 'CarbonDanger', lnum_idx, highlight[2], -1)
util.confirm({
row = cursor.line.lnum,
col = highlight[2],
highlight = 'CarbonDanger',
actions = {
{
label = 'delete',
shortcut = 'D',
callback = function()
local result =
vim.fn.delete(target.path, target.is_directory and 'rf' or '')
if result == -1 then
vim.api.nvim_echo({
{ 'Failed to delete: ', 'CarbonDanger' },
{ vim.fn.fnamemodify(target.path, ':.'), 'CarbonIndicator' },
}, false, {})
else
view.resync(vim.fn.fnamemodify(target.path, ':h'))
end
end,
},
{
label = 'cancel',
shortcut = 'q',
callback = function()
util.clear_extmarks(0, { lnum_idx, 0 }, { lnum_idx, -1 }, {})
for _, lhl in ipairs(cursor.line.highlights) do
util.add_highlight(0, lhl[1], lnum_idx, lhl[2], lhl[3])
end
self:render()
end,
},
},
})
end
function view:move()
local ctx = self:cursor()
local target_line = ctx.target_line
local targets = vim.list_extend(
{ unpack(target_line.path) },
{ target_line.entry }
)
local target_names = vim.tbl_map(function(part)
return part.name
end, targets)
if ctx.target.path == self.root.path then
return
end
local path_start = target_line.depth * 2 + 2 + target_line.icon_width
local lnum_idx = target_line.lnum - 1
local target_idx = util.tbl_key(targets, ctx.target)
local clamped_names = { unpack(target_names, 1, target_idx - 1) }
local start_hl = path_start + #table.concat(clamped_names, '/')
if target_idx > 1 then
start_hl = start_hl + 1
end
util.clear_extmarks(0, { lnum_idx, start_hl }, { lnum_idx, -1 }, {})
util.add_highlight(0, 'CarbonPending', lnum_idx, start_hl, -1)
vim.cmd.redraw({ bang = true })
vim.cmd.echohl('CarbonPending')
local updated_path = string.gsub(
vim.fn.input({
prompt = 'destination: ',
default = ctx.target.path,
cancelreturn = ctx.target.path,
}),
'/+$',
''
)
vim.cmd.echohl('None')
vim.api.nvim_echo({ { ' ' } }, false, {})
if updated_path == ctx.target.path then
self:render()
elseif vim.loop.fs_stat(updated_path) then
self:render()
vim.api.nvim_echo({
{ 'Failed to move: ', 'CarbonDanger' },
{ vim.fn.fnamemodify(ctx.target.path, ':.'), 'CarbonIndicator' },
{ ' => ' },
{ vim.fn.fnamemodify(updated_path, ':.'), 'CarbonIndicator' },
{ ' (destination exists)', 'CarbonPending' },
}, false, {})
else
local directory = vim.fn.fnamemodify(updated_path, ':h')
local tmp_path = ctx.target.path
if vim.startswith(updated_path, tmp_path) then
tmp_path = vim.fn.tempname()
vim.fn.rename(ctx.target.path, tmp_path)
end
vim.fn.mkdir(directory, 'p')
vim.fn.rename(tmp_path, updated_path)
view.resync(vim.fn.fnamemodify(ctx.target.path, ':h'))
end
end
function view:parents(count)
local path = self.root.path
local parents = {}
if path ~= '' then
for _ = count or vim.v.count1, 1, -1 do
path = vim.fn.fnamemodify(path, ':h')
parents[#parents + 1] = entry.new(path)
if path == '/' then
break
end
end
end
return parents
end
function view:switch_to_existing_view(path)
local destination_view = view.find(path)
if destination_view then
vim.api.nvim_win_set_buf(0, destination_view:buffer())
if settings.sync_pwd and self.root.path == vim.loop.cwd() then
vim.api.nvim_set_current_dir(destination_view.root.path)
end
return true
end
end
return view

View file

@ -0,0 +1,87 @@
local util = require('carbon.util')
local watcher = {}
watcher.listeners = {}
watcher.events = {}
function watcher.keep(callback)
for path in pairs(watcher.listeners) do
if not callback(path) then
watcher.release(path)
end
end
end
function watcher.release(path)
if not path then
for listener_path in pairs(watcher.listeners) do
watcher.release(listener_path)
end
elseif watcher.listeners[path] then
watcher.listeners[path]:stop()
watcher.listeners[path] = nil
end
end
function watcher.register(path)
if not watcher.listeners[path] and not util.is_excluded(path) then
watcher.listeners[path] = vim.loop.new_fs_event()
watcher.listeners[path]:start(
path,
{},
vim.schedule_wrap(function(error, filename)
watcher.emit('carbon:synchronize', path, filename, error)
end)
)
end
end
function watcher.emit(event, ...)
for callback in pairs(watcher.events[event] or {}) do
callback(event, ...)
end
for callback in pairs(watcher.events['*'] or {}) do
callback(event, ...)
end
end
function watcher.on(event, callback)
if type(event) == 'table' then
for _, key in ipairs(event) do
watcher.on(key, callback)
end
elseif event then
watcher.events[event] = watcher.events[event] or {}
watcher.events[event][callback] = callback
end
end
function watcher.off(event, callback)
if not event then
watcher.events = {}
elseif type(event) == 'table' then
for _, key in ipairs(event) do
watcher.off(key, callback)
end
elseif watcher.events[event] and callback then
watcher.events[event][callback] = nil
elseif watcher.events[event] then
watcher.events[event] = {}
else
watcher.events[event] = nil
end
end
function watcher.has(event, callback)
return watcher.events[event] and watcher.events[event][callback] and true
or false
end
function watcher.registered()
return vim.tbl_keys(watcher.listeners)
end
return watcher