perf(windows): use a single powershell process for trash operations (#271)
* perf(trash_windows): use a single powershell instance for operations * refactor(trash_windows): encapsulate powershell connection logic * refactor(windows_trash): better name for functions * fix(windows_trash): set connection error on initializatino if needed * refactor(windows_trash): simplify initialization code * refactor: extract some powershell logic into separate file --------- Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
This commit is contained in:
parent
8bc37bac29
commit
e71b6caa95
3 changed files with 197 additions and 81 deletions
|
|
@ -5,6 +5,7 @@ local config = require("oil.config")
|
|||
local constants = require("oil.constants")
|
||||
local files = require("oil.adapters.files")
|
||||
local fs = require("oil.fs")
|
||||
local powershell_trash = require("oil.adapters.trash.windows.powershell-trash")
|
||||
|
||||
local FIELD_META = constants.FIELD_META
|
||||
local FIELD_TYPE = constants.FIELD_TYPE
|
||||
|
|
@ -35,61 +36,6 @@ local win_addslash = function(path)
|
|||
end
|
||||
end
|
||||
|
||||
---@class oil.WindowsRawEntry
|
||||
---@field IsFolder boolean
|
||||
---@field DeletionDate integer
|
||||
---@field Name string
|
||||
---@field Path string
|
||||
---@field OriginalPath string
|
||||
|
||||
---@param cb fun(err?: string, raw_entries: oil.WindowsRawEntry[]?)
|
||||
local get_raw_entries = function(cb)
|
||||
---@type string?
|
||||
local stdout
|
||||
|
||||
local jid = vim.fn.jobstart({
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
-- The first line configures Windows Powershell to use UTF-8 for input and output
|
||||
-- 0xa is the constant for Recycle Bin. See https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants
|
||||
[[
|
||||
$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding
|
||||
$shell = New-Object -ComObject 'Shell.Application'
|
||||
$folder = $shell.NameSpace(0xa)
|
||||
$data = @(foreach ($i in $folder.items())
|
||||
{
|
||||
@{
|
||||
IsFolder=$i.IsFolder;
|
||||
DeletionDate=([DateTimeOffset]$i.extendedproperty('datedeleted')).ToUnixTimeSeconds();
|
||||
Name=$i.Name;
|
||||
Path=$i.Path;
|
||||
OriginalPath=-join($i.ExtendedProperty('DeletedFrom'), "\", $i.Name)
|
||||
}
|
||||
})
|
||||
ConvertTo-Json $data
|
||||
]],
|
||||
}, {
|
||||
stdout_buffered = true,
|
||||
on_stdout = function(_, data)
|
||||
stdout = table.concat(data, "\n")
|
||||
end,
|
||||
on_exit = function(_, code)
|
||||
if code ~= 0 then
|
||||
return cb("Error listing files on trash")
|
||||
end
|
||||
assert(stdout)
|
||||
local raw_entries = vim.json.decode(stdout)
|
||||
cb(nil, raw_entries)
|
||||
end,
|
||||
})
|
||||
if jid <= 0 then
|
||||
cb("Could not list windows devices")
|
||||
end
|
||||
end
|
||||
|
||||
---@class oil.WindowsTrashInfo
|
||||
---@field trash_file string?
|
||||
---@field original_path string?
|
||||
|
|
@ -106,7 +52,7 @@ M.list = function(url, column_defs, cb)
|
|||
local trash_dir = get_trash_dir()
|
||||
local show_all_files = fs.is_subpath(path, trash_dir)
|
||||
|
||||
get_raw_entries(function(err, raw_entries)
|
||||
powershell_trash.list_raw_entries(function(err, raw_entries)
|
||||
if err then
|
||||
cb(err)
|
||||
return
|
||||
|
|
@ -457,31 +403,7 @@ M.supported_cross_adapter_actions = { files = "move" }
|
|||
---@param path string
|
||||
---@param cb fun(err?: string)
|
||||
M.delete_to_trash = function(path, cb)
|
||||
local jid = vim.fn.jobstart({
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
-- 0 is the constant for Windows Desktop. See https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants
|
||||
([[
|
||||
$path = Get-Item '%s'
|
||||
$shell = New-Object -ComObject 'Shell.Application'
|
||||
$folder = $shell.NameSpace(0)
|
||||
$folder.ParseName($path.FullName).InvokeVerb('delete')
|
||||
]]):format(path:gsub("'", "''")),
|
||||
}, {
|
||||
stdout_buffered = true,
|
||||
on_exit = function(_, code)
|
||||
if code ~= 0 then
|
||||
return cb("Error sendig file to trash")
|
||||
end
|
||||
cb()
|
||||
end,
|
||||
})
|
||||
if jid <= 0 then
|
||||
cb("Could not list windows devices")
|
||||
end
|
||||
powershell_trash.delete_to_trash(path, cb)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
114
lua/oil/adapters/trash/windows/powershell-connection.lua
Normal file
114
lua/oil/adapters/trash/windows/powershell-connection.lua
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
---@class (exact) oil.PowershellCommand
|
||||
---@field cmd string
|
||||
---@field cb fun(err?: string, output?: string)
|
||||
---@field running? boolean
|
||||
|
||||
---@class oil.PowershellConnection
|
||||
---@field private jid integer
|
||||
---@field private execution_error? string
|
||||
---@field private commands oil.PowershellCommand[]
|
||||
---@field private stdout string[]
|
||||
---@field private is_reading_data boolean
|
||||
local PowershellConnection = {}
|
||||
|
||||
---@param init_command? string
|
||||
---@return oil.PowershellConnection
|
||||
function PowershellConnection.new(init_command)
|
||||
local self = setmetatable({
|
||||
commands = {},
|
||||
stdout = {},
|
||||
is_reading_data = false,
|
||||
}, { __index = PowershellConnection })
|
||||
|
||||
self:_init(init_command)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@param init_command? string
|
||||
function PowershellConnection:_init(init_command)
|
||||
local jid = vim.fn.jobstart({
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-NoLogo",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-NoExit",
|
||||
"-Command",
|
||||
"-",
|
||||
}, {
|
||||
---@param data string[]
|
||||
on_stdout = function(_, data)
|
||||
for _, fragment in ipairs(data) do
|
||||
if fragment:find("===DONE%((%a+)%)===") then
|
||||
self.is_reading_data = false
|
||||
local output = table.concat(self.stdout, "")
|
||||
local cb = self.commands[1].cb
|
||||
table.remove(self.commands, 1)
|
||||
local success = fragment:match("===DONE%((%a+)%)===")
|
||||
if success == "True" then
|
||||
cb(nil, output)
|
||||
elseif success == "False" then
|
||||
cb(success .. ": " .. output, output)
|
||||
end
|
||||
self.stdout = {}
|
||||
self:_consume()
|
||||
elseif self.is_reading_data then
|
||||
table.insert(self.stdout, fragment)
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
if jid == 0 then
|
||||
self:_set_error("passed invalid arguments to 'powershell'")
|
||||
elseif jid == -1 then
|
||||
self:_set_error("'powershell' is not executable")
|
||||
else
|
||||
self.jid = jid
|
||||
end
|
||||
|
||||
if init_command then
|
||||
table.insert(self.commands, { cmd = init_command, cb = function() end })
|
||||
self:_consume()
|
||||
end
|
||||
end
|
||||
|
||||
---@param command string
|
||||
---@param cb fun(err?: string, output?: string[])
|
||||
function PowershellConnection:run(command, cb)
|
||||
if self.execution_error then
|
||||
cb(self.execution_error)
|
||||
else
|
||||
table.insert(self.commands, { cmd = command, cb = cb })
|
||||
self:_consume()
|
||||
end
|
||||
end
|
||||
|
||||
function PowershellConnection:_consume()
|
||||
if not vim.tbl_isempty(self.commands) then
|
||||
local cmd = self.commands[1]
|
||||
if not cmd.running then
|
||||
cmd.running = true
|
||||
self.is_reading_data = true
|
||||
-- $? contains the execution status of the last command.
|
||||
-- see https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.4#section-1
|
||||
vim.api.nvim_chan_send(self.jid, cmd.cmd .. '\nWrite-Host "===DONE($?)==="\n')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param err string
|
||||
function PowershellConnection:_set_error(err)
|
||||
if self.execution_error then
|
||||
return
|
||||
end
|
||||
self.execution_error = err
|
||||
local commands = self.commands
|
||||
self.commands = {}
|
||||
for _, cmd in ipairs(commands) do
|
||||
cmd.cb(err)
|
||||
end
|
||||
end
|
||||
|
||||
return PowershellConnection
|
||||
80
lua/oil/adapters/trash/windows/powershell-trash.lua
Normal file
80
lua/oil/adapters/trash/windows/powershell-trash.lua
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
-- A wrapper around trash operations using windows powershell
|
||||
local Powershell = require("oil.adapters.trash.windows.powershell-connection")
|
||||
|
||||
---@class oil.WindowsRawEntry
|
||||
---@field IsFolder boolean
|
||||
---@field DeletionDate integer
|
||||
---@field Name string
|
||||
---@field Path string
|
||||
---@field OriginalPath string
|
||||
|
||||
local M = {}
|
||||
|
||||
-- The first line configures Windows Powershell to use UTF-8 for input and output
|
||||
-- 0xa is the constant for Recycle Bin. See https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants
|
||||
local list_entries_init = [[
|
||||
$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding
|
||||
$shell = New-Object -ComObject 'Shell.Application'
|
||||
$folder = $shell.NameSpace(0xa)
|
||||
]]
|
||||
|
||||
local list_entries_cmd = [[
|
||||
$data = @(foreach ($i in $folder.items())
|
||||
{
|
||||
@{
|
||||
IsFolder=$i.IsFolder;
|
||||
DeletionDate=([DateTimeOffset]$i.extendedproperty('datedeleted')).ToUnixTimeSeconds();
|
||||
Name=$i.Name;
|
||||
Path=$i.Path;
|
||||
OriginalPath=-join($i.ExtendedProperty('DeletedFrom'), "\", $i.Name)
|
||||
}
|
||||
})
|
||||
ConvertTo-Json $data -Compress
|
||||
]]
|
||||
|
||||
---@type nil|oil.PowershellConnection
|
||||
local list_entries_powershell
|
||||
|
||||
---@param cb fun(err?: string, raw_entries: oil.WindowsRawEntry[]?)
|
||||
M.list_raw_entries = function(cb)
|
||||
if not list_entries_powershell then
|
||||
list_entries_powershell = Powershell.new(list_entries_init)
|
||||
end
|
||||
list_entries_powershell:run(list_entries_cmd, function(err, string)
|
||||
if err then
|
||||
cb(err)
|
||||
return
|
||||
end
|
||||
|
||||
local ok, value = pcall(vim.json.decode, string)
|
||||
if not ok then
|
||||
cb(value)
|
||||
return
|
||||
end
|
||||
cb(nil, value)
|
||||
end)
|
||||
end
|
||||
|
||||
-- 0 is the constant for Windows Desktop. See https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants
|
||||
local delete_init = [[
|
||||
$shell = New-Object -ComObject 'Shell.Application'
|
||||
$folder = $shell.NameSpace(0)
|
||||
]]
|
||||
local delete_cmd = [[
|
||||
$path = Get-Item '%s'
|
||||
$folder.ParseName($path.FullName).InvokeVerb('delete')
|
||||
]]
|
||||
|
||||
---@type nil|oil.PowershellConnection
|
||||
local delete_to_trash_powershell
|
||||
|
||||
---@param path string
|
||||
---@param cb fun(err?: string)
|
||||
M.delete_to_trash = function(path, cb)
|
||||
if not delete_to_trash_powershell then
|
||||
delete_to_trash_powershell = Powershell.new(delete_init)
|
||||
end
|
||||
delete_to_trash_powershell:run((delete_cmd):format(path:gsub("'", "''")), cb)
|
||||
end
|
||||
|
||||
return M
|
||||
Loading…
Add table
Add a link
Reference in a new issue