From e71b6caa95bd29225536df64fdcd8fb0f758bb09 Mon Sep 17 00:00:00 2001 From: TheLeoP <53507599+TheLeoP@users.noreply.github.com> Date: Mon, 15 Jan 2024 20:22:11 -0500 Subject: [PATCH] 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 --- lua/oil/adapters/trash/windows.lua | 84 +------------ .../trash/windows/powershell-connection.lua | 114 ++++++++++++++++++ .../trash/windows/powershell-trash.lua | 80 ++++++++++++ 3 files changed, 197 insertions(+), 81 deletions(-) create mode 100644 lua/oil/adapters/trash/windows/powershell-connection.lua create mode 100644 lua/oil/adapters/trash/windows/powershell-trash.lua diff --git a/lua/oil/adapters/trash/windows.lua b/lua/oil/adapters/trash/windows.lua index 97bec8f4..ecfca61c 100644 --- a/lua/oil/adapters/trash/windows.lua +++ b/lua/oil/adapters/trash/windows.lua @@ -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 diff --git a/lua/oil/adapters/trash/windows/powershell-connection.lua b/lua/oil/adapters/trash/windows/powershell-connection.lua new file mode 100644 index 00000000..aeb426f1 --- /dev/null +++ b/lua/oil/adapters/trash/windows/powershell-connection.lua @@ -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 diff --git a/lua/oil/adapters/trash/windows/powershell-trash.lua b/lua/oil/adapters/trash/windows/powershell-trash.lua new file mode 100644 index 00000000..87a7465e --- /dev/null +++ b/lua/oil/adapters/trash/windows/powershell-trash.lua @@ -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