feat: first draft

This commit is contained in:
Steven Arcangeli 2022-12-15 02:24:27 -08:00
parent bf2dfb970d
commit fefd6ad5e4
48 changed files with 7201 additions and 1 deletions

View file

@ -0,0 +1,71 @@
local fs = require("oil.fs")
local ReplLayout = require("oil.repl_layout")
local M = {}
M.show = function(callback)
local marker_file = fs.join(vim.fn.stdpath("cache"), ".oil_accepted_disclaimer")
vim.loop.fs_stat(
marker_file,
vim.schedule_wrap(function(err, stat)
if stat and stat.type and not err then
callback(true)
return
end
local confirmation = "I understand this may destroy my files"
local lines = {
"WARNING",
"This plugin has been tested thoroughly, but it is still new.",
"There is a chance that there may be bugs that could lead to data loss.",
"I recommend that you ONLY use it for files that are checked in to version control.",
"",
string.format('Please type: "%s" below', confirmation),
"",
}
local hints = {
"Try again",
"Not quite!",
"It's right there ^^^^^^^^^^^",
"...seriously?",
"Just type this ^^^^",
}
local attempt = 0
local repl
repl = ReplLayout.new({
lines = lines,
on_submit = function(line)
if line:upper() ~= confirmation:upper() then
attempt = attempt % #hints + 1
vim.api.nvim_buf_set_lines(repl.input_bufnr, 0, -1, true, {})
vim.bo[repl.view_bufnr].modifiable = true
vim.api.nvim_buf_set_lines(repl.view_bufnr, 6, 7, true, { hints[attempt] })
vim.bo[repl.view_bufnr].modifiable = false
vim.bo[repl.view_bufnr].modified = false
else
fs.mkdirp(vim.fn.fnamemodify(marker_file, ":h"))
fs.touch(
marker_file,
vim.schedule_wrap(function(err2)
if err2 then
vim.notify(
string.format("Error recording response: %s", err2),
vim.log.levels.WARN
)
end
callback(true)
repl:close()
end)
)
end
end,
on_cancel = function()
callback(false)
end,
})
local ns = vim.api.nvim_create_namespace("Oil")
vim.api.nvim_buf_add_highlight(repl.view_bufnr, ns, "DiagnosticError", 0, 0, -1)
end)
)
end
return M

507
lua/oil/mutator/init.lua Normal file
View file

@ -0,0 +1,507 @@
local cache = require("oil.cache")
local columns = require("oil.columns")
local config = require("oil.config")
local disclaimer = require("oil.mutator.disclaimer")
local oil = require("oil")
local parser = require("oil.mutator.parser")
local pathutil = require("oil.pathutil")
local preview = require("oil.mutator.preview")
local Progress = require("oil.mutator.progress")
local Trie = require("oil.mutator.trie")
local util = require("oil.util")
local view = require("oil.view")
local FIELD = require("oil.constants").FIELD
local M = {}
---@alias oil.Action oil.CreateAction|oil.DeleteAction|oil.MoveAction|oil.CopyAction|oil.ChangeAction
---@class oil.CreateAction
---@field type "create"
---@field url string
---@field entry_type oil.EntryType
---@field link nil|string
---@class oil.DeleteAction
---@field type "delete"
---@field url string
---@field entry_type oil.EntryType
---@class oil.MoveAction
---@field type "move"
---@field entry_type oil.EntryType
---@field src_url string
---@field dest_url string
---@class oil.CopyAction
---@field type "copy"
---@field entry_type oil.EntryType
---@field src_url string
---@field dest_url string
---@class oil.ChangeAction
---@field type "change"
---@field entry_type oil.EntryType
---@field url string
---@field column string
---@field value any
---@param all_diffs table<integer, oil.Diff[]>
---@return oil.Action[]
M.create_actions_from_diffs = function(all_diffs)
---@type oil.Action[]
local actions = {}
local diff_by_id = setmetatable({}, {
__index = function(t, key)
local list = {}
rawset(t, key, list)
return list
end,
})
for bufnr, diffs in pairs(all_diffs) do
local adapter = util.get_adapter(bufnr)
if not adapter then
error("Missing adapter")
end
local parent_url = vim.api.nvim_buf_get_name(bufnr)
for _, diff in ipairs(diffs) do
if diff.type == "new" then
if diff.id then
local by_id = diff_by_id[diff.id]
-- FIXME this is kind of a hack. We shouldn't be setting undocumented fields on the diff
diff.dest = parent_url .. diff.name
table.insert(by_id, diff)
else
-- Parse nested files like foo/bar/baz
local pieces = vim.split(diff.name, "/")
local url = parent_url:gsub("/$", "")
for i, v in ipairs(pieces) do
local is_last = i == #pieces
local entry_type = is_last and diff.entry_type or "directory"
local alternation = v:match("{([^}]+)}")
if is_last and alternation then
-- Parse alternations like foo.{js,test.js}
for _, alt in ipairs(vim.split(alternation, ",")) do
local alt_url = url .. "/" .. v:gsub("{[^}]+}", alt)
table.insert(actions, {
type = "create",
url = alt_url,
entry_type = entry_type,
link = diff.link,
})
end
else
url = url .. "/" .. v
table.insert(actions, {
type = "create",
url = url,
entry_type = entry_type,
link = diff.link,
})
end
end
end
elseif diff.type == "change" then
table.insert(actions, {
type = "change",
url = parent_url .. diff.name,
entry_type = diff.entry_type,
column = diff.column,
value = diff.value,
})
else
local by_id = diff_by_id[diff.id]
by_id.has_delete = true
-- Don't insert the delete. We already know that there is a delete because of the presense
-- in the diff_by_id map. The list will only include the 'new' diffs.
end
end
end
for id, diffs in pairs(diff_by_id) do
local entry = cache.get_entry_by_id(id)
if not entry then
error(string.format("Could not find entry %d", id))
end
if diffs.has_delete then
local has_create = #diffs > 0
if has_create then
-- MOVE (+ optional copies) when has both creates and delete
for i, diff in ipairs(diffs) do
table.insert(actions, {
type = i == #diffs and "move" or "copy",
entry_type = entry[FIELD.type],
dest_url = diff.dest,
src_url = cache.get_parent_url(id) .. entry[FIELD.name],
})
end
else
-- DELETE when no create
table.insert(actions, {
type = "delete",
entry_type = entry[FIELD.type],
url = cache.get_parent_url(id) .. entry[FIELD.name],
})
end
else
-- COPY when create but no delete
for _, diff in ipairs(diffs) do
table.insert(actions, {
type = "copy",
entry_type = entry[FIELD.type],
src_url = cache.get_parent_url(id) .. entry[FIELD.name],
dest_url = diff.dest,
})
end
end
end
return M.enforce_action_order(actions)
end
---@param actions oil.Action[]
---@return oil.Action[]
M.enforce_action_order = function(actions)
local src_trie = Trie.new()
local dest_trie = Trie.new()
for _, action in ipairs(actions) do
if action.type == "delete" or action.type == "change" then
src_trie:insert_action(action.url, action)
elseif action.type == "create" then
dest_trie:insert_action(action.url, action)
else
dest_trie:insert_action(action.dest_url, action)
src_trie:insert_action(action.src_url, action)
end
end
-- 1. create a graph, each node points to all of its dependencies
-- 2. for each action, if not added, find it in the graph
-- 3. traverse through the graph until you reach a node that has no dependencies (leaf)
-- 4. append that action to the return value, and remove it from the graph
-- a. TODO optimization: check immediate parents to see if they have no dependencies now
-- 5. repeat
-- Gets the dependencies of a particular action. Effectively dynamically calculates the dependency
-- "edges" of the graph.
local function get_deps(action)
local ret = {}
if action.type == "delete" then
return ret
elseif action.type == "create" then
-- Finish operating on parents first
-- e.g. NEW /a BEFORE NEW /a/b
dest_trie:accum_first_parents_of(action.url, ret)
-- Process remove path before creating new path
-- e.g. DELETE /a BEFORE NEW /a
src_trie:accum_actions_at(action.url, ret, function(a)
return a.type == "move" or a.type == "delete"
end)
elseif action.type == "change" then
-- Finish operating on parents first
-- e.g. NEW /a BEFORE CHANGE /a/b
dest_trie:accum_first_parents_of(action.url, ret)
-- Finish operations on this path first
-- e.g. NEW /a BEFORE CHANGE /a
dest_trie:accum_actions_at(action.url, ret)
-- Finish copy from operations first
-- e.g. COPY /a -> /b BEFORE CHANGE /a
src_trie:accum_actions_at(action.url, ret, function(entry)
return entry.type == "copy"
end)
elseif action.type == "move" then
-- Finish operating on parents first
-- e.g. NEW /a BEFORE MOVE /z -> /a/b
dest_trie:accum_first_parents_of(action.dest_url, ret)
-- Process children before moving
-- e.g. NEW /a/b BEFORE MOVE /a -> /b
dest_trie:accum_children_of(action.src_url, ret)
-- Copy children before moving parent dir
-- e.g. COPY /a/b -> /b BEFORE MOVE /a -> /d
src_trie:accum_children_of(action.src_url, ret, function(a)
return a.type == "copy"
end)
-- Process remove path before moving to new path
-- e.g. MOVE /a -> /b BEFORE MOVE /c -> /a
src_trie:accum_actions_at(action.dest_url, ret, function(a)
return a.type == "move" or a.type == "delete"
end)
elseif action.type == "copy" then
-- Finish operating on parents first
-- e.g. NEW /a BEFORE COPY /z -> /a/b
dest_trie:accum_first_parents_of(action.dest_url, ret)
-- Process children before copying
-- e.g. NEW /a/b BEFORE COPY /a -> /b
dest_trie:accum_children_of(action.src_url, ret)
-- Process remove path before copying to new path
-- e.g. MOVE /a -> /b BEFORE COPY /c -> /a
src_trie:accum_actions_at(action.dest_url, ret, function(a)
return a.type == "move" or a.type == "delete"
end)
end
return ret
end
---@return nil|oil.Action The leaf action
---@return nil|oil.Action When no leaves found, this is the last action in the loop
local function find_leaf(action, seen)
if not seen then
seen = {}
elseif seen[action] then
return nil, action
end
seen[action] = true
local deps = get_deps(action)
if vim.tbl_isempty(deps) then
return action
end
local action_in_loop
for _, dep in ipairs(deps) do
local leaf, loop_action = find_leaf(dep, seen)
if leaf then
return leaf
elseif not action_in_loop and loop_action then
action_in_loop = loop_action
end
end
return nil, action_in_loop
end
local ret = {}
local after = {}
while not vim.tbl_isempty(actions) do
local action = actions[1]
local selected, loop_action = find_leaf(action)
local to_remove
if selected then
to_remove = selected
else
if loop_action and loop_action.type == "move" then
-- If this is moving a parent into itself, that's an error
if vim.startswith(loop_action.dest_url, loop_action.src_url) then
error("Detected cycle in desired paths")
end
-- We've detected a move cycle (e.g. MOVE /a -> /b + MOVE /b -> /a)
-- Split one of the moves and retry
local intermediate_url =
string.format("%s__oil_tmp_%05d", loop_action.src_url, math.random(999999))
local move_1 = {
type = "move",
entry_type = loop_action.entry_type,
src_url = loop_action.src_url,
dest_url = intermediate_url,
}
local move_2 = {
type = "move",
entry_type = loop_action.entry_type,
src_url = intermediate_url,
dest_url = loop_action.dest_url,
}
to_remove = loop_action
table.insert(actions, move_1)
table.insert(after, move_2)
dest_trie:insert_action(move_1.dest_url, move_1)
src_trie:insert_action(move_1.src_url, move_1)
else
error("Detected cycle in desired paths")
end
end
if selected then
if selected.type == "move" or selected.type == "copy" then
if vim.startswith(selected.dest_url, selected.src_url .. "/") then
error(
string.format(
"Cannot move or copy parent into itself: %s -> %s",
selected.src_url,
selected.dest_url
)
)
end
end
table.insert(ret, selected)
end
if to_remove then
if to_remove.type == "delete" or to_remove.type == "change" then
src_trie:remove_action(to_remove.url, to_remove)
elseif to_remove.type == "create" then
dest_trie:remove_action(to_remove.url, to_remove)
else
dest_trie:remove_action(to_remove.dest_url, to_remove)
src_trie:remove_action(to_remove.src_url, to_remove)
end
for i, a in ipairs(actions) do
if a == to_remove then
table.remove(actions, i)
break
end
end
end
end
vim.list_extend(ret, after)
return ret
end
---@param actions oil.Action[]
---@param cb fun(err: nil|string)
M.process_actions = function(actions, cb)
-- convert delete actions to move-to-trash
local trash_url = config.get_trash_url()
if trash_url then
for i, v in ipairs(actions) do
if v.type == "delete" then
local scheme, path = util.parse_url(v.url)
if config.adapters[scheme] == "files" then
actions[i] = {
type = "move",
src_url = v.url,
entry_type = v.entry_type,
dest_url = trash_url .. "/" .. pathutil.basename(path) .. string.format(
"_%06d",
math.random(999999)
),
}
end
end
end
end
-- Convert cross-adapter moves to a copy + delete
for _, action in ipairs(actions) do
if action.type == "move" then
local src_scheme = util.parse_url(action.src_url)
local dest_scheme = util.parse_url(action.dest_url)
if src_scheme ~= dest_scheme then
action.type = "copy"
table.insert(actions, {
type = "delete",
url = action.src_url,
entry_type = action.entry_type,
})
end
end
end
local finished = false
local progress = Progress.new()
-- Defer showing the progress to avoid flicker for fast operations
vim.defer_fn(function()
if not finished then
progress:show()
end
end, 100)
local function finish(...)
finished = true
progress:close()
cb(...)
end
local idx = 1
local next_action
next_action = function()
if idx > #actions then
finish()
return
end
local action = actions[idx]
progress:set_action(action, idx, #actions)
idx = idx + 1
local ok, adapter = pcall(util.get_adapter_for_action, action)
if not ok then
return finish(adapter)
end
local callback = vim.schedule_wrap(function(err)
if err then
finish(err)
else
cache.perform_action(action)
next_action()
end
end)
if action.type == "change" then
columns.perform_change_action(adapter, action, callback)
else
adapter.perform_action(action, callback)
end
end
next_action()
end
---@param confirm nil|boolean
M.try_write_changes = function(confirm)
local buffers = view.get_all_buffers()
local all_diffs = {}
local all_errors = {}
for _, bufnr in ipairs(buffers) do
-- Lock the buffer to prevent race conditions
vim.bo[bufnr].modifiable = false
if vim.bo[bufnr].modified then
local diffs, errors = parser.parse(bufnr)
all_diffs[bufnr] = diffs
if not vim.tbl_isempty(errors) then
all_errors[bufnr] = errors
end
end
end
local ns = vim.api.nvim_create_namespace("Oil")
vim.diagnostic.reset(ns)
if not vim.tbl_isempty(all_errors) then
vim.notify("Error parsing oil buffers", vim.log.levels.ERROR)
for bufnr, errors in pairs(all_errors) do
vim.diagnostic.set(ns, bufnr, errors)
end
-- Jump to an error
local curbuf = vim.api.nvim_get_current_buf()
if all_errors[curbuf] then
pcall(
vim.api.nvim_win_set_cursor,
0,
{ all_errors[curbuf][1].lnum + 1, all_errors[curbuf][1].col }
)
else
local bufnr, errs = next(pairs(all_errors))
vim.api.nvim_win_set_buf(0, bufnr)
pcall(vim.api.nvim_win_set_cursor, 0, { errs[1].lnum + 1, errs[1].col })
end
return
end
local actions = M.create_actions_from_diffs(all_diffs)
disclaimer.show(function(disclaimed)
if not disclaimed then
return
end
preview.show(actions, confirm, function(proceed)
if not proceed then
return
end
M.process_actions(
actions,
vim.schedule_wrap(function(err)
if err then
vim.notify(string.format("[oil] Error applying actions: %s", err), vim.log.levels.ERROR)
else
local current_entry = oil.get_cursor_entry()
if current_entry then
-- get the entry under the cursor and make sure the cursor stays on it
view.set_last_cursor(
vim.api.nvim_buf_get_name(0),
vim.split(current_entry.name, "/")[1]
)
end
view.rerender_visible_and_cleanup({ preserve_undo = M.trash })
end
end)
)
end)
end)
end
return M

225
lua/oil/mutator/parser.lua Normal file
View file

@ -0,0 +1,225 @@
local cache = require("oil.cache")
local columns = require("oil.columns")
local util = require("oil.util")
local view = require("oil.view")
local FIELD = require("oil.constants").FIELD
local M = {}
---@alias oil.Diff oil.DiffNew|oil.DiffDelete|oil.DiffChange
---@class oil.DiffNew
---@field type "new"
---@field name string
---@field entry_type oil.EntryType
---@field id nil|integer
---@field link nil|string
---@class oil.DiffDelete
---@field type "delete"
---@field name string
---@field id integer
---
---@class oil.DiffChange
---@field type "change"
---@field entry_type oil.EntryType
---@field name string
---@field column string
---@field value any
---@param name string
---@return string
---@return boolean
local function parsedir(name)
local isdir = vim.endswith(name, "/")
if isdir then
name = name:sub(1, name:len() - 1)
end
return name, isdir
end
---Parse a single line in a buffer
---@param adapter oil.Adapter
---@param line string
---@param column_defs oil.ColumnSpec[]
---@return table
---@return nil|oil.InternalEntry
M.parse_line = function(adapter, line, column_defs)
local ret = {}
local value, rem = line:match("^/(%d+) (.+)$")
if not value then
return nil, nil, "Malformed ID at start of line"
end
ret.id = tonumber(value)
for _, def in ipairs(column_defs) do
local name = util.split_config(def)
value, rem = columns.parse_col(adapter, rem, def)
if not value then
return nil, nil, string.format("Parsing %s failed", name)
end
ret[name] = value
end
local name = rem
if name then
local isdir
name, isdir = parsedir(vim.trim(name))
if name ~= "" then
ret.name = name
end
ret._type = isdir and "directory" or "file"
end
local entry = cache.get_entry_by_id(ret.id)
if not entry then
return ret
end
-- Parse the symlink syntax
local meta = entry[FIELD.meta]
local entry_type = entry[FIELD.type]
if entry_type == "link" and meta and meta.link then
local name_pieces = vim.split(ret.name, " -> ", { plain = true })
if #name_pieces ~= 2 then
ret.name = ""
return ret
end
ret.name = parsedir(vim.trim(name_pieces[1]))
ret.link_target = name_pieces[2]
ret._type = "link"
end
-- Try to keep the same file type
if entry_type ~= "directory" and entry_type ~= "file" and ret._type ~= "directory" then
ret._type = entry[FIELD.type]
end
return ret, entry
end
---@param bufnr integer
---@return oil.Diff[]
---@return table[] Parsing errors
M.parse = function(bufnr)
local diffs = {}
local errors = {}
local bufname = vim.api.nvim_buf_get_name(bufnr)
local adapter = util.get_adapter(bufnr)
if not adapter then
table.insert(errors, {
lnum = 1,
col = 0,
message = string.format("Cannot parse buffer '%s': No adapter", bufname),
})
return diffs, errors
end
local scheme, path = util.parse_url(bufname)
local column_defs = columns.get_supported_columns(scheme)
local parent_url = scheme .. path
local children = cache.list_url(parent_url)
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
local original_entries = {}
for _, child in pairs(children) do
if view.should_display(child) then
original_entries[child[FIELD.name]] = child[FIELD.id]
end
end
for i, line in ipairs(lines) do
if line:match("^/%d+") then
local parsed_entry, entry, err = M.parse_line(adapter, line, column_defs)
if err then
table.insert(errors, {
message = err,
lnum = i - 1,
col = 0,
})
goto continue
end
if not parsed_entry.name or parsed_entry.name:match("/") or not entry then
local message
if not parsed_entry.name then
message = "No filename found"
elseif not entry then
message = "Could not find existing entry (was the ID changed?)"
else
message = "Filename cannot contain '/'"
end
table.insert(errors, {
message = message,
lnum = i,
col = 0,
})
goto continue
end
local meta = entry[FIELD.meta]
if original_entries[parsed_entry.name] == parsed_entry.id then
if entry[FIELD.type] == "link" and (not meta or meta.link ~= parsed_entry.link_target) then
table.insert(diffs, {
type = "new",
name = parsed_entry.name,
entry_type = "link",
link = parsed_entry.link_target,
})
else
original_entries[parsed_entry.name] = nil
end
else
table.insert(diffs, {
type = "new",
name = parsed_entry.name,
entry_type = parsed_entry._type,
id = parsed_entry.id,
link = parsed_entry.link_target,
})
end
for _, col_def in ipairs(column_defs) do
local col_name = util.split_config(col_def)
if columns.compare(adapter, col_name, entry, parsed_entry[col_name]) then
table.insert(diffs, {
type = "change",
name = parsed_entry.name,
entry_type = entry[FIELD.type],
column = col_name,
value = parsed_entry[col_name],
})
end
end
else
local name, isdir = parsedir(vim.trim(line))
if vim.startswith(name, "/") then
table.insert(errors, {
message = "Paths cannot start with '/'",
lnum = i,
col = 0,
})
goto continue
end
if name ~= "" then
local link_pieces = vim.split(name, " -> ", { plain = true })
local entry_type = isdir and "directory" or "file"
local link
if #link_pieces == 2 then
entry_type = "link"
name, link = unpack(link_pieces)
end
table.insert(diffs, {
type = "new",
name = name,
entry_type = entry_type,
link = link,
})
end
end
::continue::
end
for name, child_id in pairs(original_entries) do
table.insert(diffs, {
type = "delete",
name = name,
id = child_id,
})
end
return diffs, errors
end
return M

130
lua/oil/mutator/preview.lua Normal file
View file

@ -0,0 +1,130 @@
local columns = require("oil.columns")
local config = require("oil.config")
local util = require("oil.util")
local M = {}
---@param actions oil.Action[]
---@return boolean
local function is_simple_edit(actions)
local num_create = 0
local num_copy = 0
local num_move = 0
for _, action in ipairs(actions) do
-- If there are any deletes, it is not a simple edit
if action.type == "delete" then
return false
elseif action.type == "create" then
num_create = num_create + 1
elseif action.type == "copy" then
num_copy = num_copy + 1
-- Cross-adapter copies are not simple
if util.parse_url(action.src_url) ~= util.parse_url(action.dest_url) then
return false
end
elseif action.type == "move" then
num_move = num_move + 1
-- Cross-adapter moves are not simple
if util.parse_url(action.src_url) ~= util.parse_url(action.dest_url) then
return false
end
end
end
-- More than one move/copy is complex
if num_move > 1 or num_copy > 1 then
return false
end
-- More than 5 creates is complex
if num_create > 5 then
return false
end
return true
end
---@param actions oil.Action[]
---@param should_confirm nil|boolean
---@param cb fun(proceed: boolean)
M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
if should_confirm == false then
cb(true)
return
end
if should_confirm == nil and config.skip_confirm_for_simple_edits and is_simple_edit(actions) then
cb(true)
return
end
-- The schedule wrap ensures that we actually enter the floating window.
-- Not sure why it doesn't work without that
local bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].bufhidden = "wipe"
local width = 120
local height = 40
local winid = vim.api.nvim_open_win(bufnr, true, {
relative = "editor",
width = width,
height = height,
row = math.floor((vim.o.lines - vim.o.cmdheight - height) / 2),
col = math.floor((vim.o.columns - width) / 2),
style = "minimal",
border = "rounded",
})
vim.bo[bufnr].syntax = "oil_preview"
local lines = {}
for _, action in ipairs(actions) do
local adapter = util.get_adapter_for_action(action)
if action.type == "change" then
table.insert(lines, columns.render_change_action(adapter, action))
else
table.insert(lines, adapter.render_action(action))
end
end
table.insert(lines, "")
width = vim.api.nvim_win_get_width(0)
local last_line = "[O]k [C]ancel"
local highlights = {}
local padding = string.rep(" ", math.floor((width - last_line:len()) / 2))
last_line = padding .. last_line
table.insert(highlights, { "Special", #lines, padding:len(), padding:len() + 3 })
table.insert(highlights, { "Special", #lines, padding:len() + 8, padding:len() + 11 })
table.insert(lines, last_line)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
vim.bo[bufnr].modified = false
vim.bo[bufnr].modifiable = false
local ns = vim.api.nvim_create_namespace("Oil")
for _, hl in ipairs(highlights) do
vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl))
end
local cancel
local confirm
local function make_callback(value)
return function()
confirm = function() end
cancel = function() end
vim.api.nvim_win_close(winid, true)
cb(value)
end
end
cancel = make_callback(false)
confirm = make_callback(true)
vim.api.nvim_create_autocmd("BufLeave", {
callback = cancel,
once = true,
nested = true,
buffer = bufnr,
})
vim.api.nvim_create_autocmd("WinLeave", {
callback = cancel,
once = true,
nested = true,
})
vim.keymap.set("n", "q", cancel, { buffer = bufnr })
vim.keymap.set("n", "C", cancel, { buffer = bufnr })
vim.keymap.set("n", "c", cancel, { buffer = bufnr })
vim.keymap.set("n", "<Esc>", cancel, { buffer = bufnr })
vim.keymap.set("n", "<C-c>", cancel, { buffer = bufnr })
vim.keymap.set("n", "O", confirm, { buffer = bufnr })
vim.keymap.set("n", "o", confirm, { buffer = bufnr })
end)
return M

View file

@ -0,0 +1,77 @@
local columns = require("oil.columns")
local loading = require("oil.loading")
local util = require("oil.util")
local Progress = {}
local FPS = 20
function Progress.new()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].bufhidden = "wipe"
return setmetatable({
lines = { "", "", "" },
bufnr = bufnr,
}, {
__index = Progress,
})
end
function Progress:show()
if self.winid and vim.api.nvim_win_is_valid(self.winid) then
return
end
local loading_iter = loading.get_bar_iter()
self.timer = vim.loop.new_timer()
self.timer:start(
0,
math.floor(1000 / FPS),
vim.schedule_wrap(function()
self.lines[2] = loading_iter()
self:_render()
end)
)
local width = 120
local height = 10
self.winid = vim.api.nvim_open_win(self.bufnr, true, {
relative = "editor",
width = width,
height = height,
row = math.floor((vim.o.lines - vim.o.cmdheight - height) / 2),
col = math.floor((vim.o.columns - width) / 2),
style = "minimal",
border = "rounded",
})
end
function Progress:_render()
util.render_centered_text(self.bufnr, self.lines)
end
---@param action oil.Action
---@param idx integer
---@param total integer
function Progress:set_action(action, idx, total)
local adapter = util.get_adapter_for_action(action)
local change_line
if action.type == "change" then
change_line = columns.render_change_action(adapter, action)
else
change_line = adapter.render_action(action)
end
self.lines[1] = change_line
self.lines[3] = string.format("[%d/%d]", idx, total)
self:_render()
end
function Progress:close()
if self.timer then
self.timer:close()
self.timer = nil
end
if self.winid then
vim.api.nvim_win_close(self.winid, true)
self.winid = nil
end
end
return Progress

153
lua/oil/mutator/trie.lua Normal file
View file

@ -0,0 +1,153 @@
local util = require("oil.util")
local Trie = {}
Trie.new = function()
return setmetatable({
root = { values = {}, children = {} },
}, {
__index = Trie,
})
end
---@param url string
---@return string[]
function Trie:_url_to_path_pieces(url)
local scheme, path = util.parse_url(url)
local pieces = vim.split(path, "/")
table.insert(pieces, 1, scheme)
return pieces
end
---@param url string
---@param value any
function Trie:insert_action(url, value)
local pieces = self:_url_to_path_pieces(url)
self:insert(pieces, value)
end
---@param url string
---@param value any
function Trie:remove_action(url, value)
local pieces = self:_url_to_path_pieces(url)
self:remove(pieces, value)
end
---@param path_pieces string[]
---@param value any
function Trie:insert(path_pieces, value)
local current = self.root
for _, piece in ipairs(path_pieces) do
local next_container = current.children[piece]
if not next_container then
next_container = { values = {}, children = {} }
current.children[piece] = next_container
end
current = next_container
end
table.insert(current.values, value)
end
---@param path_pieces string[]
---@param value any
function Trie:remove(path_pieces, value)
local current = self.root
for _, piece in ipairs(path_pieces) do
local next_container = current.children[piece]
if not next_container then
next_container = { values = {}, children = {} }
current.children[piece] = next_container
end
current = next_container
end
for i, v in ipairs(current.values) do
if v == value then
table.remove(current.values, i)
-- if vim.tbl_isempty(current.values) and vim.tbl_isempty(current.children) then
-- TODO remove container from trie
-- end
return
end
end
error("Value not present in trie: " .. vim.inspect(value))
end
---Add the first action that affects a parent path of the url
---@param url string
---@param ret oil.InternalEntry[]
function Trie:accum_first_parents_of(url, ret)
local pieces = self:_url_to_path_pieces(url)
local containers = { self.root }
for _, piece in ipairs(pieces) do
local next_container = containers[#containers].children[piece]
table.insert(containers, next_container)
end
table.remove(containers)
while not vim.tbl_isempty(containers) do
local container = containers[#containers]
if not vim.tbl_isempty(container.values) then
vim.list_extend(ret, container.values)
break
end
table.remove(containers)
end
end
---Do a depth-first-search and add all children matching the filter
function Trie:_dfs(container, ret, filter)
if filter then
for _, action in ipairs(container.values) do
if filter(action) then
table.insert(ret, action)
end
end
else
vim.list_extend(ret, container.values)
end
for _, child in ipairs(container.children) do
self:_dfs(child, ret)
end
end
---Add all actions affecting children of the url
---@param url string
---@param ret oil.InternalEntry[]
---@param filter nil|fun(entry: oil.InternalEntry): boolean
function Trie:accum_children_of(url, ret, filter)
local pieces = self:_url_to_path_pieces(url)
local current = self.root
for _, piece in ipairs(pieces) do
current = current.children[piece]
if not current then
return
end
end
if current then
for _, child in pairs(current.children) do
self:_dfs(child, ret, filter)
end
end
end
---Add all actions at a specific path
---@param url string
---@param ret oil.InternalEntry[]
---@param filter nil|fun(entry: oil.InternalEntry): boolean
function Trie:accum_actions_at(url, ret, filter)
local pieces = self:_url_to_path_pieces(url)
local current = self.root
for _, piece in ipairs(pieces) do
current = current.children[piece]
if not current then
return
end
end
if current then
for _, action in ipairs(current.values) do
if not filter or filter(action) then
table.insert(ret, action)
end
end
end
end
return Trie