Skip to content

Commit

Permalink
perf(windows): use a single powershell process for trash operations (#…
Browse files Browse the repository at this point in the history
…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 <[email protected]>
  • Loading branch information
TheLeoP and stevearc authored Jan 16, 2024
1 parent 8bc37ba commit e71b6ca
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 81 deletions.
84 changes: 3 additions & 81 deletions lua/oil/adapters/trash/windows.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?
Expand All @@ -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
Expand Down Expand Up @@ -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 changes: 114 additions & 0 deletions lua/oil/adapters/trash/windows/powershell-connection.lua
Original file line number Diff line number Diff line change
@@ -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 changes: 80 additions & 0 deletions lua/oil/adapters/trash/windows/powershell-trash.lua
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit e71b6ca

Please sign in to comment.