refactor: revert module namespace from canola back to oil
Problem: the canola rename creates unnecessary friction for users
migrating from stevearc/oil.nvim — every `require('oil')` call and
config reference must change.
Solution: revert all module paths, URL schemes, autocmd groups,
highlight groups, and filetype names back to `oil`. The repo stays
`canola.nvim` for identity; the code is a drop-in replacement.
This commit is contained in:
parent
9298b48c5d
commit
8dd67f91e8
68 changed files with 1622 additions and 1625 deletions
205
lua/oil/mutator/confirmation.lua
Normal file
205
lua/oil/mutator/confirmation.lua
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
local columns = require('oil.columns')
|
||||
local config = require('oil.config')
|
||||
local layout = require('oil.layout')
|
||||
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 winid integer
|
||||
---@param bufnr integer
|
||||
---@param lines string[]
|
||||
local function render_lines(winid, bufnr, lines)
|
||||
util.render_text(bufnr, lines, {
|
||||
v_align = 'top',
|
||||
h_align = 'left',
|
||||
winid = winid,
|
||||
actions = { '[Y]es', '[N]o' },
|
||||
})
|
||||
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)
|
||||
-- The schedule wrap ensures that we actually enter the floating window.
|
||||
-- Not sure why it doesn't work without that
|
||||
if should_confirm == false or #actions == 0 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
|
||||
if should_confirm == nil and config.skip_confirm_for_delete then
|
||||
local all_deletes = true
|
||||
for _, action in ipairs(actions) do
|
||||
if action.type ~= 'delete' then
|
||||
all_deletes = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if all_deletes then
|
||||
cb(true)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Create the buffer
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[bufnr].bufhidden = 'wipe'
|
||||
local lines = {}
|
||||
local max_line_width = 0
|
||||
for _, action in ipairs(actions) do
|
||||
local adapter = util.get_adapter_for_action(action)
|
||||
local line
|
||||
if action.type == 'change' then
|
||||
---@cast action oil.ChangeAction
|
||||
line = columns.render_change_action(adapter, action)
|
||||
else
|
||||
line = adapter.render_action(action)
|
||||
end
|
||||
-- We can't handle lines with newlines in them
|
||||
line = line:gsub('\n', '')
|
||||
table.insert(lines, line)
|
||||
local line_width = vim.api.nvim_strwidth(line)
|
||||
if line_width > max_line_width then
|
||||
max_line_width = line_width
|
||||
end
|
||||
end
|
||||
table.insert(lines, '')
|
||||
|
||||
-- Create the floating window
|
||||
local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.confirmation)
|
||||
local ok, winid = pcall(vim.api.nvim_open_win, bufnr, true, {
|
||||
relative = 'editor',
|
||||
width = width,
|
||||
height = height,
|
||||
row = math.floor((layout.get_editor_height() - height) / 2),
|
||||
col = math.floor((layout.get_editor_width() - width) / 2),
|
||||
zindex = 152, -- render on top of the floating window title
|
||||
style = 'minimal',
|
||||
border = config.confirmation.border,
|
||||
})
|
||||
if not ok then
|
||||
vim.notify(string.format('Error showing oil preview window: %s', winid), vim.log.levels.ERROR)
|
||||
cb(false)
|
||||
end
|
||||
vim.bo[bufnr].filetype = 'oil_preview'
|
||||
vim.bo[bufnr].syntax = 'oil_preview'
|
||||
for k, v in pairs(config.confirmation.win_options) do
|
||||
vim.api.nvim_set_option_value(k, v, { scope = 'local', win = winid })
|
||||
end
|
||||
|
||||
render_lines(winid, bufnr, lines)
|
||||
|
||||
local restore_cursor = util.hide_cursor()
|
||||
|
||||
-- Attach autocmds and keymaps
|
||||
local cancel
|
||||
local confirm
|
||||
local autocmds = {}
|
||||
local function make_callback(value)
|
||||
return function()
|
||||
confirm = function() end
|
||||
cancel = function() end
|
||||
for _, id in ipairs(autocmds) do
|
||||
vim.api.nvim_del_autocmd(id)
|
||||
end
|
||||
autocmds = {}
|
||||
vim.api.nvim_win_close(winid, true)
|
||||
restore_cursor()
|
||||
cb(value)
|
||||
end
|
||||
end
|
||||
cancel = make_callback(false)
|
||||
confirm = make_callback(true)
|
||||
vim.api.nvim_create_autocmd('BufLeave', {
|
||||
callback = function()
|
||||
cancel()
|
||||
end,
|
||||
once = true,
|
||||
nested = true,
|
||||
buffer = bufnr,
|
||||
})
|
||||
vim.api.nvim_create_autocmd('WinLeave', {
|
||||
callback = function()
|
||||
cancel()
|
||||
end,
|
||||
once = true,
|
||||
nested = true,
|
||||
})
|
||||
table.insert(
|
||||
autocmds,
|
||||
vim.api.nvim_create_autocmd('VimResized', {
|
||||
callback = function()
|
||||
if vim.api.nvim_win_is_valid(winid) then
|
||||
width, height = layout.calculate_dims(max_line_width, #lines, config.confirmation)
|
||||
vim.api.nvim_win_set_config(winid, {
|
||||
relative = 'editor',
|
||||
width = width,
|
||||
height = height,
|
||||
row = math.floor((layout.get_editor_height() - height) / 2),
|
||||
col = math.floor((layout.get_editor_width() - width) / 2),
|
||||
zindex = 152, -- render on top of the floating window title
|
||||
})
|
||||
render_lines(winid, bufnr, lines)
|
||||
end
|
||||
end,
|
||||
})
|
||||
)
|
||||
|
||||
-- We used to use [C]ancel to cancel, so preserve the old keymap
|
||||
local cancel_keys = { 'n', 'N', 'c', 'C', 'q', '<C-c>', '<Esc>' }
|
||||
for _, cancel_key in ipairs(cancel_keys) do
|
||||
vim.keymap.set('n', cancel_key, function()
|
||||
cancel()
|
||||
end, { buffer = bufnr, nowait = true })
|
||||
end
|
||||
|
||||
-- We used to use [O]k to confirm, so preserve the old keymap
|
||||
local confirm_keys = { 'y', 'Y', 'o', 'O' }
|
||||
for _, confirm_key in ipairs(confirm_keys) do
|
||||
vim.keymap.set('n', confirm_key, function()
|
||||
confirm()
|
||||
end, { buffer = bufnr, nowait = true })
|
||||
end
|
||||
end)
|
||||
|
||||
return M
|
||||
626
lua/oil/mutator/init.lua
Normal file
626
lua/oil/mutator/init.lua
Normal file
|
|
@ -0,0 +1,626 @@
|
|||
local Progress = require('oil.mutator.progress')
|
||||
local Trie = require('oil.mutator.trie')
|
||||
local cache = require('oil.cache')
|
||||
local columns = require('oil.columns')
|
||||
local config = require('oil.config')
|
||||
local confirmation = require('oil.mutator.confirmation')
|
||||
local constants = require('oil.constants')
|
||||
local fs = require('oil.fs')
|
||||
local lsp_helpers = require('oil.lsp.helpers')
|
||||
local oil = require('oil')
|
||||
local parser = require('oil.mutator.parser')
|
||||
local util = require('oil.util')
|
||||
local view = require('oil.view')
|
||||
local M = {}
|
||||
|
||||
local FIELD_NAME = constants.FIELD_NAME
|
||||
local FIELD_TYPE = constants.FIELD_TYPE
|
||||
|
||||
---@alias oil.Action oil.CreateAction|oil.DeleteAction|oil.MoveAction|oil.CopyAction|oil.ChangeAction
|
||||
|
||||
---@class (exact) oil.CreateAction
|
||||
---@field type "create"
|
||||
---@field url string
|
||||
---@field entry_type oil.EntryType
|
||||
---@field link nil|string
|
||||
|
||||
---@class (exact) oil.DeleteAction
|
||||
---@field type "delete"
|
||||
---@field url string
|
||||
---@field entry_type oil.EntryType
|
||||
|
||||
---@class (exact) oil.MoveAction
|
||||
---@field type "move"
|
||||
---@field entry_type oil.EntryType
|
||||
---@field src_url string
|
||||
---@field dest_url string
|
||||
|
||||
---@class (exact) oil.CopyAction
|
||||
---@field type "copy"
|
||||
---@field entry_type oil.EntryType
|
||||
---@field src_url string
|
||||
---@field dest_url string
|
||||
|
||||
---@class (exact) 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 = {}
|
||||
|
||||
---@type table<integer, oil.Diff[]>
|
||||
local diff_by_id = setmetatable({}, {
|
||||
__index = function(t, key)
|
||||
local list = {}
|
||||
rawset(t, key, list)
|
||||
return list
|
||||
end,
|
||||
})
|
||||
|
||||
-- To deduplicate create actions
|
||||
-- This can happen when creating deep nested files e.g.
|
||||
-- > foo/bar/a.txt
|
||||
-- > foo/bar/b.txt
|
||||
local seen_creates = {}
|
||||
|
||||
---@param action oil.Action
|
||||
local function add_action(action)
|
||||
local adapter = assert(config.get_adapter_by_scheme(action.dest_url or action.url))
|
||||
if not adapter.filter_action or adapter.filter_action(action) then
|
||||
if action.type == 'create' then
|
||||
if seen_creates[action.url] then
|
||||
return
|
||||
else
|
||||
seen_creates[action.url] = true
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(actions, action)
|
||||
end
|
||||
end
|
||||
for bufnr, diffs in pairs(all_diffs) do
|
||||
local adapter = util.get_adapter(bufnr, true)
|
||||
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]
|
||||
---HACK: set the destination on this diff for use later
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
diff.dest = parent_url .. diff.name
|
||||
table.insert(by_id, diff)
|
||||
else
|
||||
-- Parse nested files like foo/bar/baz
|
||||
local path_sep = fs.is_windows and '[/\\]' or '/'
|
||||
local pieces = vim.split(diff.name, path_sep)
|
||||
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)
|
||||
add_action({
|
||||
type = 'create',
|
||||
url = alt_url,
|
||||
entry_type = entry_type,
|
||||
link = diff.link,
|
||||
})
|
||||
end
|
||||
else
|
||||
url = url .. '/' .. v
|
||||
add_action({
|
||||
type = 'create',
|
||||
url = url,
|
||||
entry_type = entry_type,
|
||||
link = diff.link,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif diff.type == 'change' then
|
||||
add_action({
|
||||
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]
|
||||
-- HACK: set has_delete field on a list-like table of diffs
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
by_id.has_delete = true
|
||||
-- Don't insert the delete. We already know that there is a delete because of the presence
|
||||
-- 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
|
||||
---HACK: access the has_delete field on the list-like table of diffs
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
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
|
||||
add_action({
|
||||
type = i == #diffs and 'move' or 'copy',
|
||||
entry_type = entry[FIELD_TYPE],
|
||||
---HACK: access the dest field we set above
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
dest_url = diff.dest,
|
||||
src_url = cache.get_parent_url(id) .. entry[FIELD_NAME],
|
||||
})
|
||||
end
|
||||
else
|
||||
-- DELETE when no create
|
||||
add_action({
|
||||
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
|
||||
add_action({
|
||||
type = 'copy',
|
||||
entry_type = entry[FIELD_TYPE],
|
||||
src_url = cache.get_parent_url(id) .. entry[FIELD_NAME],
|
||||
---HACK: access the dest field we set above
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
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.
|
||||
---@param action oil.Action
|
||||
local function get_deps(action)
|
||||
local ret = {}
|
||||
if action.type == 'delete' then
|
||||
src_trie:accum_children_of(action.url, 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)
|
||||
-- Process children before moving parent dir
|
||||
-- e.g. COPY /a/b -> /b BEFORE MOVE /a -> /d
|
||||
-- e.g. CHANGE /a/b BEFORE MOVE /a -> /d
|
||||
src_trie:accum_children_of(action.src_url, ret)
|
||||
-- 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
|
||||
|
||||
local progress
|
||||
|
||||
---@param actions oil.Action[]
|
||||
---@param cb fun(err: nil|string)
|
||||
M.process_actions = function(actions, cb)
|
||||
vim.api.nvim_exec_autocmds(
|
||||
'User',
|
||||
{ pattern = 'OilActionsPre', modeline = false, data = { actions = actions } }
|
||||
)
|
||||
|
||||
local did_complete = nil
|
||||
if config.lsp_file_methods.enabled then
|
||||
did_complete = lsp_helpers.will_perform_file_operations(actions)
|
||||
end
|
||||
|
||||
-- Convert some cross-adapter moves to a copy + delete
|
||||
for _, action in ipairs(actions) do
|
||||
if action.type == 'move' then
|
||||
local _, cross_action = util.get_adapter_for_action(action)
|
||||
-- Only do the conversion if the cross-adapter support is "copy"
|
||||
if cross_action == 'copy' then
|
||||
---@diagnostic disable-next-line: assign-type-mismatch
|
||||
action.type = 'copy'
|
||||
table.insert(actions, {
|
||||
type = 'delete',
|
||||
url = action.src_url,
|
||||
entry_type = action.entry_type,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local finished = false
|
||||
progress = Progress.new()
|
||||
local function finish(err)
|
||||
if not finished then
|
||||
finished = true
|
||||
progress:close()
|
||||
progress = nil
|
||||
if config.cleanup_buffers_on_delete and not err then
|
||||
for _, action in ipairs(actions) do
|
||||
if action.type == 'delete' then
|
||||
local scheme, path = util.parse_url(action.url)
|
||||
if config.adapters[scheme] == 'files' then
|
||||
assert(path)
|
||||
local os_path = fs.posix_to_os_path(path)
|
||||
local bufnr = vim.fn.bufnr(os_path)
|
||||
if bufnr ~= -1 then
|
||||
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
vim.api.nvim_exec_autocmds(
|
||||
'User',
|
||||
{ pattern = 'OilActionsPost', modeline = false, data = { err = err, actions = actions } }
|
||||
)
|
||||
cb(err)
|
||||
end
|
||||
end
|
||||
|
||||
-- Defer showing the progress to avoid flicker for fast operations
|
||||
vim.defer_fn(function()
|
||||
if not finished then
|
||||
progress:show({
|
||||
-- TODO some actions are actually cancelable.
|
||||
-- We should stop them instead of stopping after the current action
|
||||
cancel = function()
|
||||
finish('Canceled')
|
||||
end,
|
||||
})
|
||||
end
|
||||
end, 100)
|
||||
|
||||
local idx = 1
|
||||
local next_action
|
||||
next_action = function()
|
||||
if finished then
|
||||
return
|
||||
end
|
||||
if idx > #actions then
|
||||
if did_complete then
|
||||
did_complete()
|
||||
end
|
||||
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 finished then
|
||||
-- This can happen if the user canceled out of the progress window
|
||||
return
|
||||
elseif err then
|
||||
finish(err)
|
||||
else
|
||||
cache.perform_action(action)
|
||||
next_action()
|
||||
end
|
||||
end)
|
||||
if action.type == 'change' then
|
||||
---@cast action oil.ChangeAction
|
||||
columns.perform_change_action(adapter, action, callback)
|
||||
else
|
||||
adapter.perform_action(action, callback)
|
||||
end
|
||||
end
|
||||
next_action()
|
||||
end
|
||||
|
||||
M.show_progress = function()
|
||||
if progress then
|
||||
progress:restore()
|
||||
end
|
||||
end
|
||||
|
||||
local mutation_in_progress = false
|
||||
|
||||
---@return boolean
|
||||
M.is_mutating = function()
|
||||
return mutation_in_progress
|
||||
end
|
||||
|
||||
---@param confirm nil|boolean
|
||||
---@param cb? fun(err: nil|string)
|
||||
M.try_write_changes = function(confirm, cb)
|
||||
if not cb then
|
||||
cb = function(_err) end
|
||||
end
|
||||
if mutation_in_progress then
|
||||
cb('Cannot perform mutation when already in progress')
|
||||
return
|
||||
end
|
||||
local current_buf = vim.api.nvim_get_current_buf()
|
||||
local was_modified = vim.bo.modified
|
||||
local buffers = view.get_all_buffers()
|
||||
local all_diffs = {}
|
||||
---@type table<integer, oil.ParseError[]>
|
||||
local all_errors = {}
|
||||
|
||||
mutation_in_progress = true
|
||||
-- Lock the buffer to prevent race conditions from the user modifying them during parsing
|
||||
view.lock_buffers()
|
||||
for _, bufnr in ipairs(buffers) do
|
||||
if vim.bo[bufnr].modified then
|
||||
local diffs, errors = parser.parse(bufnr)
|
||||
all_diffs[bufnr] = diffs
|
||||
local adapter = assert(util.get_adapter(bufnr, true))
|
||||
if adapter.filter_error then
|
||||
errors = vim.tbl_filter(adapter.filter_error, errors)
|
||||
end
|
||||
if not vim.tbl_isempty(errors) then
|
||||
all_errors[bufnr] = errors
|
||||
end
|
||||
end
|
||||
end
|
||||
local function unlock()
|
||||
view.unlock_buffers()
|
||||
-- The ":write" will set nomodified even if we cancel here, so we need to restore it
|
||||
if was_modified then
|
||||
vim.bo[current_buf].modified = true
|
||||
end
|
||||
mutation_in_progress = false
|
||||
end
|
||||
|
||||
local ns = vim.api.nvim_create_namespace('Oil')
|
||||
vim.diagnostic.reset(ns)
|
||||
if not vim.tbl_isempty(all_errors) then
|
||||
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(all_errors)
|
||||
assert(bufnr)
|
||||
assert(errs)
|
||||
-- HACK: This is a workaround for the fact that we can't switch buffers in the middle of a
|
||||
-- BufWriteCmd.
|
||||
vim.schedule(function()
|
||||
vim.api.nvim_win_set_buf(0, bufnr)
|
||||
pcall(vim.api.nvim_win_set_cursor, 0, { errs[1].lnum + 1, errs[1].col })
|
||||
end)
|
||||
end
|
||||
unlock()
|
||||
cb('Error parsing oil buffers')
|
||||
return
|
||||
end
|
||||
|
||||
local actions = M.create_actions_from_diffs(all_diffs)
|
||||
confirmation.show(actions, confirm, function(proceed)
|
||||
if not proceed then
|
||||
unlock()
|
||||
cb('Canceled')
|
||||
return
|
||||
end
|
||||
|
||||
M.process_actions(
|
||||
actions,
|
||||
vim.schedule_wrap(function(err)
|
||||
view.unlock_buffers()
|
||||
if err then
|
||||
err = string.format('[oil] Error applying actions: %s', err)
|
||||
view.rerender_all_oil_buffers(nil, function()
|
||||
cb(err)
|
||||
end)
|
||||
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.parsed_name or current_entry.name, '/')[1]
|
||||
)
|
||||
end
|
||||
view.rerender_all_oil_buffers(nil, function(render_err)
|
||||
vim.api.nvim_exec_autocmds(
|
||||
'User',
|
||||
{ pattern = 'OilMutationComplete', modeline = false }
|
||||
)
|
||||
cb(render_err)
|
||||
end)
|
||||
end
|
||||
mutation_in_progress = false
|
||||
end)
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
321
lua/oil/mutator/parser.lua
Normal file
321
lua/oil/mutator/parser.lua
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
local cache = require('oil.cache')
|
||||
local columns = require('oil.columns')
|
||||
local config = require('oil.config')
|
||||
local constants = require('oil.constants')
|
||||
local fs = require('oil.fs')
|
||||
local util = require('oil.util')
|
||||
local view = require('oil.view')
|
||||
local M = {}
|
||||
|
||||
local FIELD_ID = constants.FIELD_ID
|
||||
local FIELD_NAME = constants.FIELD_NAME
|
||||
local FIELD_TYPE = constants.FIELD_TYPE
|
||||
local FIELD_META = constants.FIELD_META
|
||||
|
||||
---@alias oil.Diff oil.DiffNew|oil.DiffDelete|oil.DiffChange
|
||||
|
||||
---@class (exact) oil.DiffNew
|
||||
---@field type "new"
|
||||
---@field name string
|
||||
---@field entry_type oil.EntryType
|
||||
---@field id nil|integer
|
||||
---@field link nil|string
|
||||
|
||||
---@class (exact) oil.DiffDelete
|
||||
---@field type "delete"
|
||||
---@field name string
|
||||
---@field id integer
|
||||
|
||||
---@class (exact) 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, '/') or (fs.is_windows and vim.endswith(name, '\\'))
|
||||
if isdir then
|
||||
name = name:sub(1, name:len() - 1)
|
||||
end
|
||||
return name, isdir
|
||||
end
|
||||
|
||||
---@param meta nil|table
|
||||
---@param parsed_entry table
|
||||
---@return boolean True if metadata and parsed entry have the same link target
|
||||
local function compare_link_target(meta, parsed_entry)
|
||||
if not meta or not meta.link then
|
||||
return false
|
||||
end
|
||||
-- Make sure we trim off any trailing path slashes from both sources
|
||||
local meta_name = meta.link:gsub('[/\\]$', '')
|
||||
local parsed_name = parsed_entry.link_target:gsub('[/\\]$', '')
|
||||
return meta_name == parsed_name
|
||||
end
|
||||
|
||||
---@class (exact) oil.ParseResult
|
||||
---@field data table Parsed entry data
|
||||
---@field ranges table<string, integer[]> Locations of the various columns
|
||||
---@field entry nil|oil.InternalEntry If the entry already exists
|
||||
|
||||
---Parse a single line in a buffer
|
||||
---@param adapter oil.Adapter
|
||||
---@param line string
|
||||
---@param column_defs oil.ColumnSpec[]
|
||||
---@return nil|oil.ParseResult
|
||||
---@return nil|string Error
|
||||
M.parse_line = function(adapter, line, column_defs)
|
||||
local ret = {}
|
||||
local ranges = {}
|
||||
local start = 1
|
||||
local value, rem = line:match('^/(%d+) (.+)$')
|
||||
if not value then
|
||||
return nil, 'Malformed ID at start of line'
|
||||
end
|
||||
ranges.id = { start, value:len() + 1 }
|
||||
start = ranges.id[2] + 1
|
||||
ret.id = tonumber(value)
|
||||
|
||||
-- Right after a mutation and we reset the cache, the parent url may not be available
|
||||
local ok, parent_url = pcall(cache.get_parent_url, ret.id)
|
||||
if ok then
|
||||
-- If this line was pasted from another adapter, it may have different columns
|
||||
local line_adapter = assert(config.get_adapter_by_scheme(parent_url))
|
||||
if adapter ~= line_adapter then
|
||||
adapter = line_adapter
|
||||
column_defs = columns.get_supported_columns(adapter)
|
||||
end
|
||||
end
|
||||
|
||||
for _, def in ipairs(column_defs) do
|
||||
local name = util.split_config(def)
|
||||
local range = { start }
|
||||
local start_len = string.len(rem)
|
||||
value, rem = columns.parse_col(adapter, assert(rem), def)
|
||||
if not rem then
|
||||
return nil, string.format('Parsing %s failed', name)
|
||||
end
|
||||
ret[name] = value
|
||||
range[2] = range[1] + start_len - string.len(rem) - 1
|
||||
ranges[name] = range
|
||||
start = range[2] + 1
|
||||
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)
|
||||
ranges.name = { start, start + string.len(rem) - 1 }
|
||||
if not entry then
|
||||
return { data = ret, ranges = ranges }
|
||||
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 { data = ret, ranges = ranges }
|
||||
end
|
||||
ranges.name = { start, start + string.len(name_pieces[1]) - 1 }
|
||||
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 { data = ret, entry = entry, ranges = ranges }
|
||||
end
|
||||
|
||||
---@class (exact) oil.ParseError
|
||||
---@field lnum integer
|
||||
---@field col integer
|
||||
---@field message string
|
||||
|
||||
---@param bufnr integer
|
||||
---@return oil.Diff[] diffs
|
||||
---@return oil.ParseError[] errors Parsing errors
|
||||
M.parse = function(bufnr)
|
||||
---@type oil.Diff[]
|
||||
local diffs = {}
|
||||
---@type oil.ParseError[]
|
||||
local errors = {}
|
||||
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
||||
local adapter = util.get_adapter(bufnr, true)
|
||||
if not adapter then
|
||||
table.insert(errors, {
|
||||
lnum = 0,
|
||||
col = 0,
|
||||
message = string.format("Cannot parse buffer '%s': No adapter", bufname),
|
||||
})
|
||||
return diffs, errors
|
||||
end
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
|
||||
local scheme, path = util.parse_url(bufname)
|
||||
local column_defs = columns.get_supported_columns(adapter)
|
||||
local parent_url = scheme .. path
|
||||
local children = cache.list_url(parent_url)
|
||||
-- map from name to entry ID for all entries previously in the buffer
|
||||
---@type table<string, integer>
|
||||
local original_entries = {}
|
||||
for _, child in pairs(children) do
|
||||
local name = child[FIELD_NAME]
|
||||
if view.should_display(bufnr, child) then
|
||||
original_entries[name] = child[FIELD_ID]
|
||||
end
|
||||
end
|
||||
local seen_names = {}
|
||||
local function check_dupe(name, i)
|
||||
if fs.is_mac or fs.is_windows then
|
||||
-- mac and windows use case-insensitive filesystems
|
||||
name = name:lower()
|
||||
end
|
||||
if seen_names[name] then
|
||||
table.insert(errors, { message = 'Duplicate filename', lnum = i - 1, end_lnum = i, col = 0 })
|
||||
else
|
||||
seen_names[name] = true
|
||||
end
|
||||
end
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
-- hack to be compatible with Lua 5.1
|
||||
-- use return instead of goto
|
||||
(function()
|
||||
if line:match('^/%d+') then
|
||||
-- Parse the line for an existing entry
|
||||
local result, err = M.parse_line(adapter, line, column_defs)
|
||||
if not result or err then
|
||||
table.insert(errors, {
|
||||
message = err,
|
||||
lnum = i - 1,
|
||||
end_lnum = i,
|
||||
col = 0,
|
||||
})
|
||||
return
|
||||
elseif result.data.id == 0 then
|
||||
-- Ignore entries with ID 0 (typically the "../" entry)
|
||||
return
|
||||
end
|
||||
local parsed_entry = result.data
|
||||
local entry = result.entry
|
||||
|
||||
local err_message
|
||||
if not parsed_entry.name then
|
||||
err_message = 'No filename found'
|
||||
elseif not entry then
|
||||
err_message = 'Could not find existing entry (was the ID changed?)'
|
||||
elseif parsed_entry.name:match('/') or parsed_entry.name:match(fs.sep) then
|
||||
err_message = 'Filename cannot contain path separator'
|
||||
end
|
||||
if err_message then
|
||||
table.insert(errors, {
|
||||
message = err_message,
|
||||
lnum = i - 1,
|
||||
end_lnum = i,
|
||||
col = 0,
|
||||
})
|
||||
return
|
||||
end
|
||||
assert(entry)
|
||||
|
||||
check_dupe(parsed_entry.name, i)
|
||||
local meta = entry[FIELD_META]
|
||||
if original_entries[parsed_entry.name] == parsed_entry.id then
|
||||
if entry[FIELD_TYPE] == 'link' and not compare_link_target(meta, parsed_entry) then
|
||||
table.insert(diffs, {
|
||||
type = 'new',
|
||||
name = parsed_entry.name,
|
||||
entry_type = 'link',
|
||||
link = parsed_entry.link_target,
|
||||
})
|
||||
elseif entry[FIELD_TYPE] ~= parsed_entry._type then
|
||||
table.insert(diffs, {
|
||||
type = 'new',
|
||||
name = parsed_entry.name,
|
||||
entry_type = parsed_entry._type,
|
||||
})
|
||||
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
|
||||
-- Parse a new entry
|
||||
local name, isdir = parsedir(vim.trim(line))
|
||||
if vim.startswith(name, '/') then
|
||||
table.insert(errors, {
|
||||
message = "Paths cannot start with '/'",
|
||||
lnum = i - 1,
|
||||
end_lnum = i,
|
||||
col = 0,
|
||||
})
|
||||
return
|
||||
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
|
||||
check_dupe(name, i)
|
||||
table.insert(diffs, {
|
||||
type = 'new',
|
||||
name = name,
|
||||
entry_type = entry_type,
|
||||
link = link,
|
||||
})
|
||||
end
|
||||
end
|
||||
end)()
|
||||
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
|
||||
223
lua/oil/mutator/progress.lua
Normal file
223
lua/oil/mutator/progress.lua
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
local columns = require('oil.columns')
|
||||
local config = require('oil.config')
|
||||
local layout = require('oil.layout')
|
||||
local loading = require('oil.loading')
|
||||
local util = require('oil.util')
|
||||
local Progress = {}
|
||||
|
||||
local FPS = 20
|
||||
|
||||
function Progress.new()
|
||||
return setmetatable({
|
||||
lines = { '', '' },
|
||||
count = '',
|
||||
spinner = '',
|
||||
bufnr = nil,
|
||||
winid = nil,
|
||||
min_bufnr = nil,
|
||||
min_winid = nil,
|
||||
autocmds = {},
|
||||
closing = false,
|
||||
}, {
|
||||
__index = Progress,
|
||||
})
|
||||
end
|
||||
|
||||
---@private
|
||||
---@return boolean
|
||||
function Progress:is_minimized()
|
||||
return not self.closing
|
||||
and not self.bufnr
|
||||
and self.min_bufnr
|
||||
and vim.api.nvim_buf_is_valid(self.min_bufnr)
|
||||
end
|
||||
|
||||
---@param opts nil|table
|
||||
--- cancel fun()
|
||||
function Progress:show(opts)
|
||||
opts = opts or {}
|
||||
if self.winid and vim.api.nvim_win_is_valid(self.winid) then
|
||||
return
|
||||
end
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[bufnr].bufhidden = 'wipe'
|
||||
self.bufnr = bufnr
|
||||
self.cancel = opts.cancel or self.cancel
|
||||
local loading_iter = loading.get_bar_iter()
|
||||
local spinner = loading.get_iter('dots')
|
||||
if not self.timer then
|
||||
self.timer = vim.loop.new_timer()
|
||||
self.timer:start(
|
||||
0,
|
||||
math.floor(1000 / FPS),
|
||||
vim.schedule_wrap(function()
|
||||
self.lines[2] = string.format('%s %s', self.count, loading_iter())
|
||||
self.spinner = spinner()
|
||||
self:_render()
|
||||
end)
|
||||
)
|
||||
end
|
||||
local width, height = layout.calculate_dims(120, 10, config.progress)
|
||||
self.winid = vim.api.nvim_open_win(self.bufnr, true, {
|
||||
relative = 'editor',
|
||||
width = width,
|
||||
height = height,
|
||||
row = math.floor((layout.get_editor_height() - height) / 2),
|
||||
col = math.floor((layout.get_editor_width() - width) / 2),
|
||||
zindex = 152, -- render on top of the floating window title
|
||||
style = 'minimal',
|
||||
border = config.progress.border,
|
||||
})
|
||||
vim.bo[self.bufnr].filetype = 'oil_progress'
|
||||
for k, v in pairs(config.progress.win_options) do
|
||||
vim.api.nvim_set_option_value(k, v, { scope = 'local', win = self.winid })
|
||||
end
|
||||
table.insert(
|
||||
self.autocmds,
|
||||
vim.api.nvim_create_autocmd('VimResized', {
|
||||
callback = function()
|
||||
self:_reposition()
|
||||
end,
|
||||
})
|
||||
)
|
||||
table.insert(
|
||||
self.autocmds,
|
||||
vim.api.nvim_create_autocmd('WinLeave', {
|
||||
callback = function()
|
||||
self:minimize()
|
||||
end,
|
||||
})
|
||||
)
|
||||
local cancel = self.cancel or function() end
|
||||
local minimize = function()
|
||||
if self.winid and vim.api.nvim_win_is_valid(self.winid) then
|
||||
vim.api.nvim_win_close(self.winid, true)
|
||||
end
|
||||
end
|
||||
vim.keymap.set('n', 'c', cancel, { buffer = self.bufnr, nowait = true })
|
||||
vim.keymap.set('n', 'C', cancel, { buffer = self.bufnr, nowait = true })
|
||||
vim.keymap.set('n', 'm', minimize, { buffer = self.bufnr, nowait = true })
|
||||
vim.keymap.set('n', 'M', minimize, { buffer = self.bufnr, nowait = true })
|
||||
end
|
||||
|
||||
function Progress:restore()
|
||||
if self.closing then
|
||||
return
|
||||
elseif not self:is_minimized() then
|
||||
error('Cannot restore progress window: not minimized')
|
||||
end
|
||||
self:_cleanup_minimized_win()
|
||||
self:show()
|
||||
end
|
||||
|
||||
function Progress:_render()
|
||||
if self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr) then
|
||||
util.render_text(
|
||||
self.bufnr,
|
||||
self.lines,
|
||||
{ winid = self.winid, actions = { '[M]inimize', '[C]ancel' } }
|
||||
)
|
||||
end
|
||||
if self.min_bufnr and vim.api.nvim_buf_is_valid(self.min_bufnr) then
|
||||
util.render_text(
|
||||
self.min_bufnr,
|
||||
{ string.format('%sOil: %s', self.spinner, self.count) },
|
||||
{ winid = self.min_winid, h_align = 'left' }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
function Progress:_reposition()
|
||||
if self.winid and vim.api.nvim_win_is_valid(self.winid) then
|
||||
local min_width = 120
|
||||
local line_width = vim.api.nvim_strwidth(self.lines[1])
|
||||
if line_width > min_width then
|
||||
min_width = line_width
|
||||
end
|
||||
local width, height = layout.calculate_dims(min_width, 10, config.progress)
|
||||
vim.api.nvim_win_set_config(self.winid, {
|
||||
relative = 'editor',
|
||||
width = width,
|
||||
height = height,
|
||||
row = math.floor((layout.get_editor_height() - height) / 2),
|
||||
col = math.floor((layout.get_editor_width() - width) / 2),
|
||||
zindex = 152, -- render on top of the floating window title
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function Progress:_cleanup_main_win()
|
||||
if self.winid then
|
||||
if vim.api.nvim_win_is_valid(self.winid) then
|
||||
vim.api.nvim_win_close(self.winid, true)
|
||||
end
|
||||
self.winid = nil
|
||||
end
|
||||
for _, id in ipairs(self.autocmds) do
|
||||
vim.api.nvim_del_autocmd(id)
|
||||
end
|
||||
self.autocmds = {}
|
||||
self.bufnr = nil
|
||||
end
|
||||
|
||||
function Progress:_cleanup_minimized_win()
|
||||
if self.min_winid and vim.api.nvim_win_is_valid(self.min_winid) then
|
||||
vim.api.nvim_win_close(self.min_winid, true)
|
||||
end
|
||||
self.min_winid = nil
|
||||
self.min_bufnr = nil
|
||||
end
|
||||
|
||||
function Progress:minimize()
|
||||
if self.closing then
|
||||
return
|
||||
end
|
||||
self:_cleanup_main_win()
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[bufnr].bufhidden = 'wipe'
|
||||
local winid = vim.api.nvim_open_win(bufnr, false, {
|
||||
relative = 'editor',
|
||||
width = 16,
|
||||
height = 1,
|
||||
anchor = 'SE',
|
||||
row = layout.get_editor_height(),
|
||||
col = layout.get_editor_width(),
|
||||
zindex = 152, -- render on top of the floating window title
|
||||
style = 'minimal',
|
||||
border = config.progress.minimized_border,
|
||||
})
|
||||
self.min_bufnr = bufnr
|
||||
self.min_winid = winid
|
||||
self:_render()
|
||||
vim.notify_once('Restore progress window with :Oil --progress')
|
||||
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
|
||||
---@cast action oil.ChangeAction
|
||||
change_line = columns.render_change_action(adapter, action)
|
||||
else
|
||||
change_line = adapter.render_action(action)
|
||||
end
|
||||
self.lines[1] = change_line
|
||||
self.count = string.format('%d/%d', idx, total)
|
||||
self:_reposition()
|
||||
self:_render()
|
||||
end
|
||||
|
||||
function Progress:close()
|
||||
self.closing = true
|
||||
if self.timer then
|
||||
self.timer:close()
|
||||
self.timer = nil
|
||||
end
|
||||
self:_cleanup_main_win()
|
||||
self:_cleanup_minimized_win()
|
||||
end
|
||||
|
||||
return Progress
|
||||
160
lua/oil/mutator/trie.lua
Normal file
160
lua/oil/mutator/trie.lua
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
local util = require('oil.util')
|
||||
|
||||
---@class (exact) oil.Trie
|
||||
---@field new fun(): oil.Trie
|
||||
---@field private root table
|
||||
local Trie = {}
|
||||
|
||||
---@return oil.Trie
|
||||
Trie.new = function()
|
||||
---@type oil.Trie
|
||||
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)
|
||||
assert(path)
|
||||
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.Action): 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? fun(entry: oil.Action): 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue