Meh I'll figure out submodules later
This commit is contained in:
parent
4ca9d44a90
commit
8cb281f436
352 changed files with 66107 additions and 0 deletions
|
|
@ -0,0 +1,289 @@
|
|||
---@meta
|
||||
---This file is autogenerated, DO NOT EDIT
|
||||
error "Cannot require a meta file"
|
||||
|
||||
---@generic T:any
|
||||
---@alias LuassertFunction fun(value:T, message?:string):T
|
||||
---@alias LuassertFunctionTwoArgs fun(expected:T, actual:T, message?:string):T
|
||||
---@alias LuassertFunctionMultiArgs fun(...:T):T
|
||||
|
||||
---@class Luassert
|
||||
---@field are_boolean LuassertFunction
|
||||
---@field are_equal LuassertFunctionTwoArgs
|
||||
---@field are_equals LuassertFunctionTwoArgs
|
||||
---@field are_error LuassertFunction
|
||||
---@field are_error_match LuassertFunctionTwoArgs
|
||||
---@field are_error_matches LuassertFunctionTwoArgs
|
||||
---@field are_errors LuassertFunction
|
||||
---@field are_false LuassertFunction
|
||||
---@field are_falsy LuassertFunction
|
||||
---@field are_function LuassertFunction
|
||||
---@field are_holes LuassertFunction
|
||||
---@field are_match LuassertFunctionTwoArgs
|
||||
---@field are_match_error LuassertFunctionTwoArgs
|
||||
---@field are_matches LuassertFunctionTwoArgs
|
||||
---@field are_matches_error LuassertFunctionTwoArgs
|
||||
---@field are_near LuassertFunctionMultiArgs
|
||||
---@field are_nil LuassertFunction
|
||||
---@field are_number LuassertFunction
|
||||
---@field are_returned_arguments LuassertFunction
|
||||
---@field are_same LuassertFunctionTwoArgs
|
||||
---@field are_string LuassertFunction
|
||||
---@field are_table LuassertFunction
|
||||
---@field are_thread LuassertFunction
|
||||
---@field are_true LuassertFunction
|
||||
---@field are_truthy LuassertFunction
|
||||
---@field are_unique LuassertFunction
|
||||
---@field are_userdata LuassertFunction
|
||||
---@field array_boolean LuassertFunction
|
||||
---@field array_equal LuassertFunctionTwoArgs
|
||||
---@field array_equals LuassertFunctionTwoArgs
|
||||
---@field array_error LuassertFunction
|
||||
---@field array_error_match LuassertFunctionTwoArgs
|
||||
---@field array_error_matches LuassertFunctionTwoArgs
|
||||
---@field array_errors LuassertFunction
|
||||
---@field array_false LuassertFunction
|
||||
---@field array_falsy LuassertFunction
|
||||
---@field array_function LuassertFunction
|
||||
---@field array_holes LuassertFunction
|
||||
---@field array_match LuassertFunctionTwoArgs
|
||||
---@field array_match_error LuassertFunctionTwoArgs
|
||||
---@field array_matches LuassertFunctionTwoArgs
|
||||
---@field array_matches_error LuassertFunctionTwoArgs
|
||||
---@field array_near LuassertFunctionMultiArgs
|
||||
---@field array_nil LuassertFunction
|
||||
---@field array_number LuassertFunction
|
||||
---@field array_returned_arguments LuassertFunction
|
||||
---@field array_same LuassertFunctionTwoArgs
|
||||
---@field array_string LuassertFunction
|
||||
---@field array_table LuassertFunction
|
||||
---@field array_thread LuassertFunction
|
||||
---@field array_true LuassertFunction
|
||||
---@field array_truthy LuassertFunction
|
||||
---@field array_unique LuassertFunction
|
||||
---@field array_userdata LuassertFunction
|
||||
---@field does_boolean LuassertFunction
|
||||
---@field does_equal LuassertFunctionTwoArgs
|
||||
---@field does_equals LuassertFunctionTwoArgs
|
||||
---@field does_error LuassertFunction
|
||||
---@field does_error_match LuassertFunctionTwoArgs
|
||||
---@field does_error_matches LuassertFunctionTwoArgs
|
||||
---@field does_errors LuassertFunction
|
||||
---@field does_false LuassertFunction
|
||||
---@field does_falsy LuassertFunction
|
||||
---@field does_function LuassertFunction
|
||||
---@field does_holes LuassertFunction
|
||||
---@field does_match LuassertFunctionTwoArgs
|
||||
---@field does_match_error LuassertFunctionTwoArgs
|
||||
---@field does_matches LuassertFunctionTwoArgs
|
||||
---@field does_matches_error LuassertFunctionTwoArgs
|
||||
---@field does_near LuassertFunctionMultiArgs
|
||||
---@field does_nil LuassertFunction
|
||||
---@field does_number LuassertFunction
|
||||
---@field does_returned_arguments LuassertFunction
|
||||
---@field does_same LuassertFunctionTwoArgs
|
||||
---@field does_string LuassertFunction
|
||||
---@field does_table LuassertFunction
|
||||
---@field does_thread LuassertFunction
|
||||
---@field does_true LuassertFunction
|
||||
---@field does_truthy LuassertFunction
|
||||
---@field does_unique LuassertFunction
|
||||
---@field does_userdata LuassertFunction
|
||||
---@field has_boolean LuassertFunction
|
||||
---@field has_equal LuassertFunctionTwoArgs
|
||||
---@field has_equals LuassertFunctionTwoArgs
|
||||
---@field has_error LuassertFunction
|
||||
---@field has_error_match LuassertFunctionTwoArgs
|
||||
---@field has_error_matches LuassertFunctionTwoArgs
|
||||
---@field has_errors LuassertFunction
|
||||
---@field has_false LuassertFunction
|
||||
---@field has_falsy LuassertFunction
|
||||
---@field has_function LuassertFunction
|
||||
---@field has_holes LuassertFunction
|
||||
---@field has_match LuassertFunctionTwoArgs
|
||||
---@field has_match_error LuassertFunctionTwoArgs
|
||||
---@field has_matches LuassertFunctionTwoArgs
|
||||
---@field has_matches_error LuassertFunctionTwoArgs
|
||||
---@field has_near LuassertFunctionMultiArgs
|
||||
---@field has_nil LuassertFunction
|
||||
---@field has_number LuassertFunction
|
||||
---@field has_returned_arguments LuassertFunction
|
||||
---@field has_same LuassertFunctionTwoArgs
|
||||
---@field has_string LuassertFunction
|
||||
---@field has_table LuassertFunction
|
||||
---@field has_thread LuassertFunction
|
||||
---@field has_true LuassertFunction
|
||||
---@field has_truthy LuassertFunction
|
||||
---@field has_unique LuassertFunction
|
||||
---@field has_userdata LuassertFunction
|
||||
---@field is_boolean LuassertFunction
|
||||
---@field is_equal LuassertFunctionTwoArgs
|
||||
---@field is_equals LuassertFunctionTwoArgs
|
||||
---@field is_error LuassertFunction
|
||||
---@field is_error_match LuassertFunctionTwoArgs
|
||||
---@field is_error_matches LuassertFunctionTwoArgs
|
||||
---@field is_errors LuassertFunction
|
||||
---@field is_false LuassertFunction
|
||||
---@field is_falsy LuassertFunction
|
||||
---@field is_function LuassertFunction
|
||||
---@field is_holes LuassertFunction
|
||||
---@field is_match LuassertFunctionTwoArgs
|
||||
---@field is_match_error LuassertFunctionTwoArgs
|
||||
---@field is_matches LuassertFunctionTwoArgs
|
||||
---@field is_matches_error LuassertFunctionTwoArgs
|
||||
---@field is_near LuassertFunctionMultiArgs
|
||||
---@field is_nil LuassertFunction
|
||||
---@field is_number LuassertFunction
|
||||
---@field is_returned_arguments LuassertFunction
|
||||
---@field is_same LuassertFunctionTwoArgs
|
||||
---@field is_string LuassertFunction
|
||||
---@field is_table LuassertFunction
|
||||
---@field is_thread LuassertFunction
|
||||
---@field is_true LuassertFunction
|
||||
---@field is_truthy LuassertFunction
|
||||
---@field is_unique LuassertFunction
|
||||
---@field is_userdata LuassertFunction
|
||||
---@field message_boolean LuassertFunction
|
||||
---@field message_equal LuassertFunctionTwoArgs
|
||||
---@field message_equals LuassertFunctionTwoArgs
|
||||
---@field message_error LuassertFunction
|
||||
---@field message_error_match LuassertFunctionTwoArgs
|
||||
---@field message_error_matches LuassertFunctionTwoArgs
|
||||
---@field message_errors LuassertFunction
|
||||
---@field message_false LuassertFunction
|
||||
---@field message_falsy LuassertFunction
|
||||
---@field message_function LuassertFunction
|
||||
---@field message_holes LuassertFunction
|
||||
---@field message_match LuassertFunctionTwoArgs
|
||||
---@field message_match_error LuassertFunctionTwoArgs
|
||||
---@field message_matches LuassertFunctionTwoArgs
|
||||
---@field message_matches_error LuassertFunctionTwoArgs
|
||||
---@field message_near LuassertFunctionMultiArgs
|
||||
---@field message_nil LuassertFunction
|
||||
---@field message_number LuassertFunction
|
||||
---@field message_returned_arguments LuassertFunction
|
||||
---@field message_same LuassertFunctionTwoArgs
|
||||
---@field message_string LuassertFunction
|
||||
---@field message_table LuassertFunction
|
||||
---@field message_thread LuassertFunction
|
||||
---@field message_true LuassertFunction
|
||||
---@field message_truthy LuassertFunction
|
||||
---@field message_unique LuassertFunction
|
||||
---@field message_userdata LuassertFunction
|
||||
---@field no_boolean LuassertFunction
|
||||
---@field no_equal LuassertFunctionTwoArgs
|
||||
---@field no_equals LuassertFunctionTwoArgs
|
||||
---@field no_error LuassertFunction
|
||||
---@field no_error_match LuassertFunctionTwoArgs
|
||||
---@field no_error_matches LuassertFunctionTwoArgs
|
||||
---@field no_errors LuassertFunction
|
||||
---@field no_false LuassertFunction
|
||||
---@field no_falsy LuassertFunction
|
||||
---@field no_function LuassertFunction
|
||||
---@field no_holes LuassertFunction
|
||||
---@field no_match LuassertFunctionTwoArgs
|
||||
---@field no_match_error LuassertFunctionTwoArgs
|
||||
---@field no_matches LuassertFunctionTwoArgs
|
||||
---@field no_matches_error LuassertFunctionTwoArgs
|
||||
---@field no_near LuassertFunctionMultiArgs
|
||||
---@field no_nil LuassertFunction
|
||||
---@field no_number LuassertFunction
|
||||
---@field no_returned_arguments LuassertFunction
|
||||
---@field no_same LuassertFunctionTwoArgs
|
||||
---@field no_string LuassertFunction
|
||||
---@field no_table LuassertFunction
|
||||
---@field no_thread LuassertFunction
|
||||
---@field no_true LuassertFunction
|
||||
---@field no_truthy LuassertFunction
|
||||
---@field no_unique LuassertFunction
|
||||
---@field no_userdata LuassertFunction
|
||||
---@field not_boolean LuassertFunction
|
||||
---@field not_equal LuassertFunctionTwoArgs
|
||||
---@field not_equals LuassertFunctionTwoArgs
|
||||
---@field not_error LuassertFunction
|
||||
---@field not_error_match LuassertFunctionTwoArgs
|
||||
---@field not_error_matches LuassertFunctionTwoArgs
|
||||
---@field not_errors LuassertFunction
|
||||
---@field not_false LuassertFunction
|
||||
---@field not_falsy LuassertFunction
|
||||
---@field not_function LuassertFunction
|
||||
---@field not_holes LuassertFunction
|
||||
---@field not_match LuassertFunctionTwoArgs
|
||||
---@field not_match_error LuassertFunctionTwoArgs
|
||||
---@field not_matches LuassertFunctionTwoArgs
|
||||
---@field not_matches_error LuassertFunctionTwoArgs
|
||||
---@field not_near LuassertFunctionMultiArgs
|
||||
---@field not_nil LuassertFunction
|
||||
---@field not_number LuassertFunction
|
||||
---@field not_returned_arguments LuassertFunction
|
||||
---@field not_same LuassertFunctionTwoArgs
|
||||
---@field not_string LuassertFunction
|
||||
---@field not_table LuassertFunction
|
||||
---@field not_thread LuassertFunction
|
||||
---@field not_true LuassertFunction
|
||||
---@field not_truthy LuassertFunction
|
||||
---@field not_unique LuassertFunction
|
||||
---@field not_userdata LuassertFunction
|
||||
---@field was_boolean LuassertFunction
|
||||
---@field was_equal LuassertFunctionTwoArgs
|
||||
---@field was_equals LuassertFunctionTwoArgs
|
||||
---@field was_error LuassertFunction
|
||||
---@field was_error_match LuassertFunctionTwoArgs
|
||||
---@field was_error_matches LuassertFunctionTwoArgs
|
||||
---@field was_errors LuassertFunction
|
||||
---@field was_false LuassertFunction
|
||||
---@field was_falsy LuassertFunction
|
||||
---@field was_function LuassertFunction
|
||||
---@field was_holes LuassertFunction
|
||||
---@field was_match LuassertFunctionTwoArgs
|
||||
---@field was_match_error LuassertFunctionTwoArgs
|
||||
---@field was_matches LuassertFunctionTwoArgs
|
||||
---@field was_matches_error LuassertFunctionTwoArgs
|
||||
---@field was_near LuassertFunctionMultiArgs
|
||||
---@field was_nil LuassertFunction
|
||||
---@field was_number LuassertFunction
|
||||
---@field was_returned_arguments LuassertFunction
|
||||
---@field was_same LuassertFunctionTwoArgs
|
||||
---@field was_string LuassertFunction
|
||||
---@field was_table LuassertFunction
|
||||
---@field was_thread LuassertFunction
|
||||
---@field was_true LuassertFunction
|
||||
---@field was_truthy LuassertFunction
|
||||
---@field was_unique LuassertFunction
|
||||
---@field was_userdata LuassertFunction
|
||||
---@field boolean LuassertFunction
|
||||
---@field equal LuassertFunctionTwoArgs
|
||||
---@field equals LuassertFunctionTwoArgs
|
||||
---@field error LuassertFunction
|
||||
---@field error_match LuassertFunctionTwoArgs
|
||||
---@field error_matches LuassertFunctionTwoArgs
|
||||
---@field errors LuassertFunction
|
||||
---@field False LuassertFunction
|
||||
---@field falsy LuassertFunction
|
||||
---@field Function LuassertFunction
|
||||
---@field holes LuassertFunction
|
||||
---@field match LuassertFunctionTwoArgs
|
||||
---@field match_error LuassertFunctionTwoArgs
|
||||
---@field matches LuassertFunctionTwoArgs
|
||||
---@field matches_error LuassertFunctionTwoArgs
|
||||
---@field near LuassertFunctionMultiArgs
|
||||
---@field Nil LuassertFunction
|
||||
---@field number LuassertFunction
|
||||
---@field returned_arguments LuassertFunction
|
||||
---@field same LuassertFunctionTwoArgs
|
||||
---@field string LuassertFunction
|
||||
---@field table LuassertFunction
|
||||
---@field thread LuassertFunction
|
||||
---@field True LuassertFunction
|
||||
---@field truthy LuassertFunction
|
||||
---@field unique LuassertFunction
|
||||
---@field userdata LuassertFunction
|
||||
---@field are Luassert
|
||||
---@field array Luassert
|
||||
---@field does Luassert
|
||||
---@field has Luassert
|
||||
---@field is Luassert
|
||||
---@field message Luassert
|
||||
---@field no Luassert
|
||||
---@field Not Luassert
|
||||
---@field was Luassert
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
local util = require "plenary.async.util"
|
||||
|
||||
return setmetatable({}, {
|
||||
__index = function(t, k)
|
||||
return function(...)
|
||||
-- if we are in a fast event await the scheduler
|
||||
if vim.in_fast_event() then
|
||||
util.scheduler()
|
||||
end
|
||||
|
||||
return vim.api[k](...)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
local co = coroutine
|
||||
local vararg = require "plenary.vararg"
|
||||
local errors = require "plenary.errors"
|
||||
local traceback_error = errors.traceback_error
|
||||
local f = require "plenary.functional"
|
||||
|
||||
local M = {}
|
||||
|
||||
local function is_callable(fn)
|
||||
return type(fn) == "function" or (type(fn) == "table" and type(getmetatable(fn)["__call"]) == "function")
|
||||
end
|
||||
|
||||
---because we can't store varargs
|
||||
local function callback_or_next(step, thread, callback, ...)
|
||||
local stat = f.first(...)
|
||||
|
||||
if not stat then
|
||||
error(string.format("The coroutine failed with this message: %s", f.second(...)))
|
||||
end
|
||||
|
||||
if co.status(thread) == "dead" then
|
||||
if callback == nil then
|
||||
return
|
||||
end
|
||||
callback(select(2, ...))
|
||||
else
|
||||
local returned_function = f.second(...)
|
||||
local nargs = f.third(...)
|
||||
|
||||
assert(is_callable(returned_function), "type error :: expected func")
|
||||
returned_function(vararg.rotate(nargs, step, select(4, ...)))
|
||||
end
|
||||
end
|
||||
|
||||
---Executes a future with a callback when it is done
|
||||
---@param async_function Future: the future to execute
|
||||
---@param callback function: the callback to call when done
|
||||
local execute = function(async_function, callback, ...)
|
||||
assert(is_callable(async_function), "type error :: expected func")
|
||||
|
||||
local thread = co.create(async_function)
|
||||
|
||||
local step
|
||||
step = function(...)
|
||||
callback_or_next(step, thread, callback, co.resume(thread, ...))
|
||||
end
|
||||
|
||||
step(...)
|
||||
end
|
||||
|
||||
local add_leaf_function
|
||||
do
|
||||
---A table to store all leaf async functions
|
||||
_PlenaryLeafTable = setmetatable({}, {
|
||||
__mode = "k",
|
||||
})
|
||||
|
||||
add_leaf_function = function(async_func, argc)
|
||||
assert(_PlenaryLeafTable[async_func] == nil, "Async function should not already be in the table")
|
||||
_PlenaryLeafTable[async_func] = argc
|
||||
end
|
||||
|
||||
function M.is_leaf_function(async_func)
|
||||
return _PlenaryLeafTable[async_func] ~= nil
|
||||
end
|
||||
|
||||
function M.get_leaf_function_argc(async_func)
|
||||
return _PlenaryLeafTable[async_func]
|
||||
end
|
||||
end
|
||||
|
||||
---Creates an async function with a callback style function.
|
||||
---@param func function: A callback style function to be converted. The last argument must be the callback.
|
||||
---@param argc number: The number of arguments of func. Must be included.
|
||||
---@return function: Returns an async function
|
||||
M.wrap = function(func, argc)
|
||||
if not is_callable(func) then
|
||||
traceback_error("type error :: expected func, got " .. type(func))
|
||||
end
|
||||
|
||||
if type(argc) ~= "number" then
|
||||
traceback_error("type error :: expected number, got " .. type(argc))
|
||||
end
|
||||
|
||||
local function leaf(...)
|
||||
local nargs = select("#", ...)
|
||||
|
||||
if nargs == argc then
|
||||
return func(...)
|
||||
else
|
||||
return co.yield(func, argc, ...)
|
||||
end
|
||||
end
|
||||
|
||||
add_leaf_function(leaf, argc)
|
||||
|
||||
return leaf
|
||||
end
|
||||
|
||||
---Use this to either run a future concurrently and then do something else
|
||||
---or use it to run a future with a callback in a non async context
|
||||
---@param async_function function
|
||||
---@param callback function
|
||||
M.run = function(async_function, callback)
|
||||
if M.is_leaf_function(async_function) then
|
||||
async_function(callback)
|
||||
else
|
||||
execute(async_function, callback)
|
||||
end
|
||||
end
|
||||
|
||||
---Use this to create a function which executes in an async context but
|
||||
---called from a non-async context. Inherently this cannot return anything
|
||||
---since it is non-blocking
|
||||
---@param func function
|
||||
M.void = function(func)
|
||||
return function(...)
|
||||
execute(func, nil, ...)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
local a = require "plenary.async.async"
|
||||
local Deque = require("plenary.async.structs").Deque
|
||||
local tbl = require "plenary.tbl"
|
||||
|
||||
local M = {}
|
||||
|
||||
local Condvar = {}
|
||||
Condvar.__index = Condvar
|
||||
|
||||
---@class Condvar
|
||||
---@return Condvar
|
||||
function Condvar.new()
|
||||
return setmetatable({ handles = {} }, Condvar)
|
||||
end
|
||||
|
||||
---`blocks` the thread until a notification is received
|
||||
Condvar.wait = a.wrap(function(self, callback)
|
||||
-- not calling the callback will block the coroutine
|
||||
table.insert(self.handles, callback)
|
||||
end, 2)
|
||||
|
||||
---notify everyone that is waiting on this Condvar
|
||||
function Condvar:notify_all()
|
||||
local len = #self.handles
|
||||
for i, callback in ipairs(self.handles) do
|
||||
if i > len then
|
||||
-- this means that more handles were added while we were notifying
|
||||
-- if we don't break we can get starvation notifying as soon as new handles are added
|
||||
break
|
||||
end
|
||||
|
||||
callback()
|
||||
end
|
||||
|
||||
for _ = 1, len do
|
||||
-- table.remove will ensure that indexes are correct and make "ipairs" safe,
|
||||
-- which is not the case for "self.handles[i] = nil"
|
||||
table.remove(self.handles)
|
||||
end
|
||||
end
|
||||
|
||||
---notify randomly one person that is waiting on this Condvar
|
||||
function Condvar:notify_one()
|
||||
if #self.handles == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local idx = math.random(#self.handles)
|
||||
self.handles[idx]()
|
||||
table.remove(self.handles, idx)
|
||||
end
|
||||
|
||||
M.Condvar = Condvar
|
||||
|
||||
local Semaphore = {}
|
||||
Semaphore.__index = Semaphore
|
||||
|
||||
---@class Semaphore
|
||||
---@param initial_permits number: the number of permits that it can give out
|
||||
---@return Semaphore
|
||||
function Semaphore.new(initial_permits)
|
||||
vim.validate {
|
||||
initial_permits = {
|
||||
initial_permits,
|
||||
function(n)
|
||||
return n > 0
|
||||
end,
|
||||
"number greater than 0",
|
||||
},
|
||||
}
|
||||
|
||||
return setmetatable({ permits = initial_permits, handles = {} }, Semaphore)
|
||||
end
|
||||
|
||||
---async function, blocks until a permit can be acquired
|
||||
---example:
|
||||
---local semaphore = Semaphore.new(1024)
|
||||
---local permit = semaphore:acquire()
|
||||
---permit:forget()
|
||||
---when a permit can be acquired returns it
|
||||
---call permit:forget() to forget the permit
|
||||
Semaphore.acquire = a.wrap(function(self, callback)
|
||||
if self.permits > 0 then
|
||||
self.permits = self.permits - 1
|
||||
else
|
||||
table.insert(self.handles, callback)
|
||||
return
|
||||
end
|
||||
|
||||
local permit = {}
|
||||
|
||||
permit.forget = function(self_permit)
|
||||
self.permits = self.permits + 1
|
||||
|
||||
if self.permits > 0 and #self.handles > 0 then
|
||||
self.permits = self.permits - 1
|
||||
table.remove(self.handles)(self_permit)
|
||||
end
|
||||
end
|
||||
|
||||
callback(permit)
|
||||
end, 2)
|
||||
|
||||
M.Semaphore = Semaphore
|
||||
|
||||
M.channel = {}
|
||||
|
||||
---Creates a oneshot channel
|
||||
---returns a sender and receiver function
|
||||
---the sender is not async while the receiver is
|
||||
---@return function, function
|
||||
M.channel.oneshot = function()
|
||||
local val = nil
|
||||
local saved_callback = nil
|
||||
local sent = false
|
||||
local received = false
|
||||
local is_single = false
|
||||
|
||||
--- sender is not async
|
||||
--- sends a value which can be nil
|
||||
local sender = function(...)
|
||||
assert(not sent, "Oneshot channel can only send once")
|
||||
sent = true
|
||||
|
||||
if saved_callback ~= nil then
|
||||
saved_callback(...)
|
||||
return
|
||||
end
|
||||
|
||||
-- optimise for when there is only one or zero argument, no need to pack
|
||||
local nargs = select("#", ...)
|
||||
if nargs == 1 or nargs == 0 then
|
||||
val = ...
|
||||
is_single = true
|
||||
else
|
||||
val = tbl.pack(...)
|
||||
end
|
||||
end
|
||||
|
||||
--- receiver is async
|
||||
--- blocks until a value is received
|
||||
local receiver = a.wrap(function(callback)
|
||||
assert(not received, "Oneshot channel can only receive one value!")
|
||||
|
||||
if sent then
|
||||
received = true
|
||||
if is_single then
|
||||
return callback(val)
|
||||
else
|
||||
return callback(tbl.unpack(val))
|
||||
end
|
||||
else
|
||||
saved_callback = callback
|
||||
end
|
||||
end, 1)
|
||||
|
||||
return sender, receiver
|
||||
end
|
||||
|
||||
---A counter channel.
|
||||
---Basically a channel that you want to use only to notify and not to send any actual values.
|
||||
---@return function: sender
|
||||
---@return function: receiver
|
||||
M.channel.counter = function()
|
||||
local counter = 0
|
||||
local condvar = Condvar.new()
|
||||
|
||||
local Sender = {}
|
||||
|
||||
function Sender:send()
|
||||
counter = counter + 1
|
||||
condvar:notify_all()
|
||||
end
|
||||
|
||||
local Receiver = {}
|
||||
|
||||
Receiver.recv = function()
|
||||
if counter == 0 then
|
||||
condvar:wait()
|
||||
end
|
||||
counter = counter - 1
|
||||
end
|
||||
|
||||
Receiver.last = function()
|
||||
if counter == 0 then
|
||||
condvar:wait()
|
||||
end
|
||||
counter = 0
|
||||
end
|
||||
|
||||
return Sender, Receiver
|
||||
end
|
||||
|
||||
---A multiple producer single consumer channel
|
||||
---@return table
|
||||
---@return table
|
||||
M.channel.mpsc = function()
|
||||
local deque = Deque.new()
|
||||
local condvar = Condvar.new()
|
||||
|
||||
local Sender = {}
|
||||
|
||||
function Sender.send(...)
|
||||
deque:pushleft { ... }
|
||||
condvar:notify_all()
|
||||
end
|
||||
|
||||
local Receiver = {}
|
||||
|
||||
Receiver.recv = function()
|
||||
if deque:is_empty() then
|
||||
condvar:wait()
|
||||
end
|
||||
return unpack(deque:popright())
|
||||
end
|
||||
|
||||
Receiver.last = function()
|
||||
if deque:is_empty() then
|
||||
condvar:wait()
|
||||
end
|
||||
local val = deque:popleft()
|
||||
deque:clear()
|
||||
return unpack(val or {})
|
||||
end
|
||||
|
||||
return Sender, Receiver
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
---@brief [[
|
||||
--- NOTE: This API is still under construction.
|
||||
--- It may change in the future :)
|
||||
---@brief ]]
|
||||
|
||||
local lookups = {
|
||||
uv = "plenary.async.uv_async",
|
||||
util = "plenary.async.util",
|
||||
lsp = "plenary.async.lsp",
|
||||
api = "plenary.async.api",
|
||||
tests = "plenary.async.tests",
|
||||
control = "plenary.async.control",
|
||||
}
|
||||
|
||||
local exports = setmetatable(require "plenary.async.async", {
|
||||
__index = function(t, k)
|
||||
local require_path = lookups[k]
|
||||
if not require_path then
|
||||
return
|
||||
end
|
||||
|
||||
local mod = require(require_path)
|
||||
t[k] = mod
|
||||
|
||||
return mod
|
||||
end,
|
||||
})
|
||||
|
||||
exports.tests.add_globals = function()
|
||||
a = exports
|
||||
|
||||
-- must prefix with a or stack overflow, plenary.test harness already added it
|
||||
a.describe = exports.tests.describe
|
||||
-- must prefix with a or stack overflow
|
||||
a.it = exports.tests.it
|
||||
a.pending = exports.tests.pending
|
||||
a.before_each = exports.tests.before_each
|
||||
a.after_each = exports.tests.after_each
|
||||
end
|
||||
|
||||
exports.tests.add_to_env = function()
|
||||
local env = getfenv(2)
|
||||
|
||||
env.a = exports
|
||||
|
||||
-- must prefix with a or stack overflow, plenary.test harness already added it
|
||||
env.a.describe = exports.tests.describe
|
||||
-- must prefix with a or stack overflow
|
||||
env.a.it = exports.tests.it
|
||||
env.a.pending = exports.tests.pending
|
||||
env.a.before_each = exports.tests.before_each
|
||||
env.a.after_each = exports.tests.after_each
|
||||
|
||||
setfenv(2, env)
|
||||
end
|
||||
|
||||
return exports
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
local a = require "plenary.async.async"
|
||||
|
||||
local M = {}
|
||||
|
||||
---This will be deprecated because the callback can be called multiple times.
|
||||
---This will give a coroutine error because the coroutine will be resumed multiple times.
|
||||
---Please use buf_request_all instead.
|
||||
M.buf_request = a.wrap(vim.lsp.buf_request, 4)
|
||||
|
||||
M.buf_request_all = a.wrap(vim.lsp.buf_request_all, 4)
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
local M = {}
|
||||
|
||||
local Deque = {}
|
||||
Deque.__index = Deque
|
||||
|
||||
---@class Deque
|
||||
---A double ended queue
|
||||
---
|
||||
---@return Deque
|
||||
function Deque.new()
|
||||
-- the indexes are created with an offset so that the indices are consequtive
|
||||
-- otherwise, when both pushleft and pushright are used, the indices will have a 1 length hole in the middle
|
||||
return setmetatable({ first = 0, last = -1 }, Deque)
|
||||
end
|
||||
|
||||
---push to the left of the deque
|
||||
---@param value any
|
||||
function Deque:pushleft(value)
|
||||
local first = self.first - 1
|
||||
self.first = first
|
||||
self[first] = value
|
||||
end
|
||||
|
||||
---push to the right of the deque
|
||||
---@param value any
|
||||
function Deque:pushright(value)
|
||||
local last = self.last + 1
|
||||
self.last = last
|
||||
self[last] = value
|
||||
end
|
||||
|
||||
---pop from the left of the deque
|
||||
---@return any
|
||||
function Deque:popleft()
|
||||
local first = self.first
|
||||
if first > self.last then
|
||||
return nil
|
||||
end
|
||||
local value = self[first]
|
||||
self[first] = nil -- to allow garbage collection
|
||||
self.first = first + 1
|
||||
return value
|
||||
end
|
||||
|
||||
---pops from the right of the deque
|
||||
---@return any
|
||||
function Deque:popright()
|
||||
local last = self.last
|
||||
if self.first > last then
|
||||
return nil
|
||||
end
|
||||
local value = self[last]
|
||||
self[last] = nil -- to allow garbage collection
|
||||
self.last = last - 1
|
||||
return value
|
||||
end
|
||||
|
||||
---checks if the deque is empty
|
||||
---@return boolean
|
||||
function Deque:is_empty()
|
||||
return self:len() == 0
|
||||
end
|
||||
|
||||
---returns the number of elements of the deque
|
||||
---@return number
|
||||
function Deque:len()
|
||||
return self.last - self.first + 1
|
||||
end
|
||||
|
||||
---returns and iterator of the indices and values starting from the left
|
||||
---@return function
|
||||
function Deque:ipairs_left()
|
||||
local i = self.first
|
||||
|
||||
return function()
|
||||
local res = self[i]
|
||||
local idx = i
|
||||
|
||||
if res then
|
||||
i = i + 1
|
||||
|
||||
return idx, res
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---returns and iterator of the indices and values starting from the right
|
||||
---@return function
|
||||
function Deque:ipairs_right()
|
||||
local i = self.last
|
||||
|
||||
return function()
|
||||
local res = self[i]
|
||||
local idx = i
|
||||
|
||||
if res then
|
||||
i = i - 1 -- advance the iterator before we return
|
||||
|
||||
return idx, res
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---removes all values from the deque
|
||||
---@return nil
|
||||
function Deque:clear()
|
||||
for i, _ in self:ipairs_left() do
|
||||
self[i] = nil
|
||||
end
|
||||
self.first = 0
|
||||
self.last = -1
|
||||
end
|
||||
|
||||
M.Deque = Deque
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
local util = require "plenary.async.util"
|
||||
|
||||
local M = {}
|
||||
|
||||
M.describe = function(s, async_func)
|
||||
describe(s, async_func)
|
||||
end
|
||||
|
||||
M.it = function(s, async_func)
|
||||
it(s, util.will_block(async_func, tonumber(vim.env.PLENARY_TEST_TIMEOUT)))
|
||||
end
|
||||
|
||||
M.pending = function(async_func)
|
||||
pending(async_func)
|
||||
end
|
||||
|
||||
M.before_each = function(async_func)
|
||||
before_each(util.will_block(async_func))
|
||||
end
|
||||
|
||||
M.after_each = function(async_func)
|
||||
after_each(util.will_block(async_func))
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
local a = require "plenary.async.async"
|
||||
local vararg = require "plenary.vararg"
|
||||
-- local control = a.control
|
||||
local control = require "plenary.async.control"
|
||||
local channel = control.channel
|
||||
|
||||
local M = {}
|
||||
|
||||
local defer_swapped = function(timeout, callback)
|
||||
vim.defer_fn(callback, timeout)
|
||||
end
|
||||
|
||||
---Sleep for milliseconds
|
||||
---@param ms number
|
||||
M.sleep = a.wrap(defer_swapped, 2)
|
||||
|
||||
---This will COMPLETELY block neovim
|
||||
---please just use a.run unless you have a very special usecase
|
||||
---for example, in plenary test_harness you must use this
|
||||
---@param async_function Future
|
||||
---@param timeout number: Stop blocking if the timeout was surpassed. Default 2000.
|
||||
M.block_on = function(async_function, timeout)
|
||||
async_function = M.protected(async_function)
|
||||
|
||||
local stat
|
||||
local ret = {}
|
||||
|
||||
a.run(async_function, function(stat_, ...)
|
||||
stat = stat_
|
||||
ret = { ... }
|
||||
end)
|
||||
|
||||
vim.wait(timeout or 2000, function()
|
||||
return stat ~= nil
|
||||
end, 20, false)
|
||||
|
||||
if stat == false then
|
||||
error(string.format("Blocking on future timed out or was interrupted.\n%s", unpack(ret)))
|
||||
end
|
||||
|
||||
return unpack(ret)
|
||||
end
|
||||
|
||||
---@see M.block_on
|
||||
---@param async_function Future
|
||||
---@param timeout number
|
||||
M.will_block = function(async_function, timeout)
|
||||
return function()
|
||||
M.block_on(async_function, timeout)
|
||||
end
|
||||
end
|
||||
|
||||
M.join = function(async_fns)
|
||||
local len = #async_fns
|
||||
local results = {}
|
||||
if len == 0 then
|
||||
return results
|
||||
end
|
||||
|
||||
local done = 0
|
||||
|
||||
local tx, rx = channel.oneshot()
|
||||
|
||||
for i, async_fn in ipairs(async_fns) do
|
||||
assert(type(async_fn) == "function", "type error :: future must be function")
|
||||
|
||||
local cb = function(...)
|
||||
results[i] = { ... }
|
||||
done = done + 1
|
||||
if done == len then
|
||||
tx()
|
||||
end
|
||||
end
|
||||
|
||||
a.run(async_fn, cb)
|
||||
end
|
||||
|
||||
rx()
|
||||
|
||||
return results
|
||||
end
|
||||
|
||||
---Returns a result from the future that finishes at the first
|
||||
---@param async_functions table: The futures that you want to select
|
||||
---@return ...
|
||||
M.run_first = a.wrap(function(async_functions, step)
|
||||
local ran = false
|
||||
|
||||
for _, async_function in ipairs(async_functions) do
|
||||
assert(type(async_function) == "function", "type error :: future must be function")
|
||||
|
||||
local callback = function(...)
|
||||
if not ran then
|
||||
ran = true
|
||||
step(...)
|
||||
end
|
||||
end
|
||||
|
||||
async_function(callback)
|
||||
end
|
||||
end, 2)
|
||||
|
||||
---Returns a result from the functions that finishes at the first
|
||||
---@param funcs table: The async functions that you want to select
|
||||
---@return ...
|
||||
M.race = function(funcs)
|
||||
local async_functions = vim.tbl_map(function(func)
|
||||
return function(callback)
|
||||
a.run(func, callback)
|
||||
end
|
||||
end, funcs)
|
||||
return M.run_first(async_functions)
|
||||
end
|
||||
|
||||
M.run_all = function(async_fns, callback)
|
||||
a.run(function()
|
||||
M.join(async_fns)
|
||||
end, callback)
|
||||
end
|
||||
|
||||
function M.apcall(async_fn, ...)
|
||||
local nargs = a.get_leaf_function_argc(async_fn)
|
||||
if nargs then
|
||||
local tx, rx = channel.oneshot()
|
||||
local stat, ret = pcall(async_fn, vararg.rotate(nargs, tx, ...))
|
||||
if not stat then
|
||||
return stat, ret
|
||||
else
|
||||
return stat, rx()
|
||||
end
|
||||
else
|
||||
return pcall(async_fn, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function M.protected(async_fn)
|
||||
return function()
|
||||
return M.apcall(async_fn)
|
||||
end
|
||||
end
|
||||
|
||||
---An async function that when called will yield to the neovim scheduler to be able to call the api.
|
||||
M.scheduler = a.wrap(vim.schedule, 1)
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
local a = require "plenary.async.async"
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
local function add(name, argc, custom)
|
||||
local success, ret = pcall(a.wrap, custom or uv[name], argc)
|
||||
|
||||
if not success then
|
||||
error("Failed to add function with name " .. name)
|
||||
end
|
||||
|
||||
M[name] = ret
|
||||
end
|
||||
|
||||
add("close", 4) -- close a handle
|
||||
|
||||
-- filesystem operations
|
||||
add("fs_open", 4)
|
||||
add("fs_read", 4)
|
||||
add("fs_close", 2)
|
||||
add("fs_unlink", 2)
|
||||
add("fs_write", 4)
|
||||
add("fs_mkdir", 3)
|
||||
add("fs_mkdtemp", 2)
|
||||
-- 'fs_mkstemp',
|
||||
add("fs_rmdir", 2)
|
||||
add("fs_scandir", 2)
|
||||
add("fs_stat", 2)
|
||||
add("fs_fstat", 2)
|
||||
add("fs_lstat", 2)
|
||||
add("fs_rename", 3)
|
||||
add("fs_fsync", 2)
|
||||
add("fs_fdatasync", 2)
|
||||
add("fs_ftruncate", 3)
|
||||
add("fs_sendfile", 5)
|
||||
add("fs_access", 3)
|
||||
add("fs_chmod", 3)
|
||||
add("fs_fchmod", 3)
|
||||
add("fs_utime", 4)
|
||||
add("fs_futime", 4)
|
||||
-- 'fs_lutime',
|
||||
add("fs_link", 3)
|
||||
add("fs_symlink", 4)
|
||||
add("fs_readlink", 2)
|
||||
add("fs_realpath", 2)
|
||||
add("fs_chown", 4)
|
||||
add("fs_fchown", 4)
|
||||
-- 'fs_lchown',
|
||||
add("fs_copyfile", 4)
|
||||
add("fs_opendir", 3, function(path, entries, callback)
|
||||
return uv.fs_opendir(path, callback, entries)
|
||||
end)
|
||||
add("fs_readdir", 2)
|
||||
add("fs_closedir", 2)
|
||||
-- 'fs_statfs',
|
||||
|
||||
-- stream
|
||||
add("shutdown", 2)
|
||||
add("listen", 3)
|
||||
-- add('read_start', 2) -- do not do this one, the callback is made multiple times
|
||||
add("write", 3)
|
||||
add("write2", 4)
|
||||
add("shutdown", 2)
|
||||
|
||||
-- tcp
|
||||
add("tcp_connect", 4)
|
||||
-- 'tcp_close_reset',
|
||||
|
||||
-- pipe
|
||||
add("pipe_connect", 3)
|
||||
|
||||
-- udp
|
||||
add("udp_send", 5)
|
||||
add("udp_recv_start", 2)
|
||||
|
||||
-- fs event (wip make into async await event)
|
||||
-- fs poll event (wip make into async await event)
|
||||
|
||||
-- dns
|
||||
add("getaddrinfo", 4)
|
||||
add("getnameinfo", 2)
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
local a = require "plenary.async_lib.async"
|
||||
local async, await = a.async, a.await
|
||||
|
||||
return setmetatable({}, {
|
||||
__index = function(t, k)
|
||||
return async(function(...)
|
||||
-- if we are in a fast event await the scheduler
|
||||
if vim.in_fast_event() then
|
||||
await(a.scheduler())
|
||||
end
|
||||
|
||||
vim.api[k](...)
|
||||
end)
|
||||
end,
|
||||
})
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
local co = coroutine
|
||||
local errors = require "plenary.errors"
|
||||
local traceback_error = errors.traceback_error
|
||||
local f = require "plenary.functional"
|
||||
local tbl = require "plenary.tbl"
|
||||
|
||||
local M = {}
|
||||
|
||||
---because we can't store varargs
|
||||
local function callback_or_next(step, thread, callback, ...)
|
||||
local stat = f.first(...)
|
||||
|
||||
if not stat then
|
||||
error(string.format("The coroutine failed with this message: %s", f.second(...)))
|
||||
end
|
||||
|
||||
if co.status(thread) == "dead" then
|
||||
(callback or function() end)(select(2, ...))
|
||||
else
|
||||
assert(select("#", select(2, ...)) == 1, "expected a single return value")
|
||||
local returned_future = f.second(...)
|
||||
assert(type(returned_future) == "function", "type error :: expected func")
|
||||
returned_future(step)
|
||||
end
|
||||
end
|
||||
|
||||
---@class Future
|
||||
---Something that will give a value when run
|
||||
|
||||
---Executes a future with a callback when it is done
|
||||
---@param future Future: the future to execute
|
||||
---@param callback function: the callback to call when done
|
||||
local execute = function(future, callback)
|
||||
assert(type(future) == "function", "type error :: expected func")
|
||||
local thread = co.create(future)
|
||||
|
||||
local step
|
||||
step = function(...)
|
||||
callback_or_next(step, thread, callback, co.resume(thread, ...))
|
||||
end
|
||||
|
||||
step()
|
||||
end
|
||||
|
||||
---Creates an async function with a callback style function.
|
||||
---@param func function: A callback style function to be converted. The last argument must be the callback.
|
||||
---@param argc number: The number of arguments of func. Must be included.
|
||||
---@return function: Returns an async function
|
||||
M.wrap = function(func, argc)
|
||||
if type(func) ~= "function" then
|
||||
traceback_error("type error :: expected func, got " .. type(func))
|
||||
end
|
||||
|
||||
if type(argc) ~= "number" and argc ~= "vararg" then
|
||||
traceback_error "expected argc to be a number or string literal 'vararg'"
|
||||
end
|
||||
|
||||
return function(...)
|
||||
local params = tbl.pack(...)
|
||||
|
||||
local function future(step)
|
||||
if step then
|
||||
if type(argc) == "number" then
|
||||
params[argc] = step
|
||||
params.n = argc
|
||||
else
|
||||
table.insert(params, step) -- change once not optional
|
||||
params.n = params.n + 1
|
||||
end
|
||||
|
||||
return func(tbl.unpack(params))
|
||||
else
|
||||
return co.yield(future)
|
||||
end
|
||||
end
|
||||
return future
|
||||
end
|
||||
end
|
||||
|
||||
---Return a new future that when run will run all futures concurrently.
|
||||
---@param futures table: the futures that you want to join
|
||||
---@return Future: returns a future
|
||||
M.join = M.wrap(function(futures, step)
|
||||
local len = #futures
|
||||
local results = {}
|
||||
local done = 0
|
||||
|
||||
if len == 0 then
|
||||
return step(results)
|
||||
end
|
||||
|
||||
for i, future in ipairs(futures) do
|
||||
assert(type(future) == "function", "type error :: future must be function")
|
||||
|
||||
local callback = function(...)
|
||||
results[i] = { ... }
|
||||
done = done + 1
|
||||
if done == len then
|
||||
step(results)
|
||||
end
|
||||
end
|
||||
|
||||
future(callback)
|
||||
end
|
||||
end, 2)
|
||||
|
||||
---Returns a future that when run will select the first future that finishes
|
||||
---@param futures table: The future that you want to select
|
||||
---@return Future
|
||||
M.select = M.wrap(function(futures, step)
|
||||
local selected = false
|
||||
|
||||
for _, future in ipairs(futures) do
|
||||
assert(type(future) == "function", "type error :: future must be function")
|
||||
|
||||
local callback = function(...)
|
||||
if not selected then
|
||||
selected = true
|
||||
step(...)
|
||||
end
|
||||
end
|
||||
|
||||
future(callback)
|
||||
end
|
||||
end, 2)
|
||||
|
||||
---Use this to either run a future concurrently and then do something else
|
||||
---or use it to run a future with a callback in a non async context
|
||||
---@param future Future
|
||||
---@param callback function
|
||||
M.run = function(future, callback)
|
||||
future(callback or function() end)
|
||||
end
|
||||
|
||||
---Same as run but runs multiple futures
|
||||
---@param futures table
|
||||
---@param callback function
|
||||
M.run_all = function(futures, callback)
|
||||
M.run(M.join(futures), callback)
|
||||
end
|
||||
|
||||
---Await a future, yielding the current function
|
||||
---@param future Future
|
||||
---@return any: returns the result of the future when it is done
|
||||
M.await = function(future)
|
||||
assert(type(future) == "function", "type error :: expected function to await")
|
||||
return future(nil)
|
||||
end
|
||||
|
||||
---Same as await but can await multiple futures.
|
||||
---If the futures have libuv leaf futures they will be run concurrently
|
||||
---@param futures table
|
||||
---@return table: returns a table of results that each future returned. Note that if the future returns multiple values they will be packed into a table.
|
||||
M.await_all = function(futures)
|
||||
assert(type(futures) == "table", "type error :: expected table")
|
||||
return M.await(M.join(futures))
|
||||
end
|
||||
|
||||
---suspend a coroutine
|
||||
M.suspend = co.yield
|
||||
|
||||
---create a async scope
|
||||
M.scope = function(func)
|
||||
M.run(M.future(func))
|
||||
end
|
||||
|
||||
--- Future a :: a -> (a -> ())
|
||||
--- turns this signature
|
||||
--- ... -> Future a
|
||||
--- into this signature
|
||||
--- ... -> ()
|
||||
M.void = function(async_func)
|
||||
return function(...)
|
||||
async_func(...)(function() end)
|
||||
end
|
||||
end
|
||||
|
||||
M.async_void = function(func)
|
||||
return M.void(M.async(func))
|
||||
end
|
||||
|
||||
---creates an async function
|
||||
---@param func function
|
||||
---@return function: returns an async function
|
||||
M.async = function(func)
|
||||
if type(func) ~= "function" then
|
||||
traceback_error("type error :: expected func, got " .. type(func))
|
||||
end
|
||||
|
||||
return function(...)
|
||||
local args = tbl.pack(...)
|
||||
local function future(step)
|
||||
if step == nil then
|
||||
return func(tbl.unpack(args))
|
||||
else
|
||||
execute(future, step)
|
||||
end
|
||||
end
|
||||
return future
|
||||
end
|
||||
end
|
||||
|
||||
---creates a future
|
||||
---@param func function
|
||||
---@return Future
|
||||
M.future = function(func)
|
||||
return M.async(func)()
|
||||
end
|
||||
|
||||
---An async function that when awaited will await the scheduler to be able to call the api.
|
||||
M.scheduler = M.wrap(vim.schedule, 1)
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
---@brief [[
|
||||
--- NOTE: This API is still under construction.
|
||||
--- It may change in the future :)
|
||||
---@brief ]]
|
||||
|
||||
local exports = require "plenary.async_lib.async"
|
||||
exports.uv = require "plenary.async_lib.uv_async"
|
||||
exports.util = require "plenary.async_lib.util"
|
||||
exports.lsp = require "plenary.async_lib.lsp"
|
||||
exports.api = require "plenary.async_lib.api"
|
||||
exports.tests = require "plenary.async_lib.tests"
|
||||
|
||||
exports.tests.add_globals = function()
|
||||
a = exports
|
||||
async = exports.async
|
||||
await = exports.await
|
||||
await_all = exports.await_all
|
||||
|
||||
-- must prefix with a or stack overflow, plenary.test harness already added it
|
||||
a.describe = exports.tests.describe
|
||||
-- must prefix with a or stack overflow
|
||||
a.it = exports.tests.it
|
||||
end
|
||||
|
||||
exports.tests.add_to_env = function()
|
||||
local env = getfenv(2)
|
||||
|
||||
env.a = exports
|
||||
env.async = exports.async
|
||||
env.await = exports.await
|
||||
env.await_all = exports.await_all
|
||||
|
||||
-- must prefix with a or stack overflow, plenary.test harness already added it
|
||||
env.a.describe = exports.tests.describe
|
||||
-- must prefix with a or stack overflow
|
||||
env.a.it = exports.tests.it
|
||||
|
||||
setfenv(2, env)
|
||||
end
|
||||
|
||||
return exports
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
local a = require "plenary.async_lib.async"
|
||||
|
||||
local M = {}
|
||||
|
||||
---This will be deprecated because the callback can be called multiple times.
|
||||
---This will give a coroutine error because the coroutine will be resumed multiple times.
|
||||
---Please use buf_request_all instead.
|
||||
M.buf_request = a.wrap(vim.lsp.buf_request, 4)
|
||||
|
||||
--This was recently merged into master so we just check if it is there
|
||||
if vim.lsp.buf_request_all ~= nil then
|
||||
M.buf_request_all = a.wrap(vim.lsp.buf_request_all, 4)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
local M = {}
|
||||
|
||||
Deque = {}
|
||||
Deque.__index = Deque
|
||||
|
||||
---@class Deque
|
||||
---A double ended queue
|
||||
---
|
||||
---@return Deque
|
||||
function Deque.new()
|
||||
-- the indexes are created with an offset so that the indices are consequtive
|
||||
-- otherwise, when both pushleft and pushright are used, the indices will have a 1 length hole in the middle
|
||||
return setmetatable({ first = 0, last = -1 }, Deque)
|
||||
end
|
||||
|
||||
---push to the left of the deque
|
||||
---@param value any
|
||||
function Deque:pushleft(value)
|
||||
local first = self.first - 1
|
||||
self.first = first
|
||||
self[first] = value
|
||||
end
|
||||
|
||||
---push to the right of the deque
|
||||
---@param value any
|
||||
function Deque:pushright(value)
|
||||
local last = self.last + 1
|
||||
self.last = last
|
||||
self[last] = value
|
||||
end
|
||||
|
||||
---pop from the left of the deque
|
||||
---@return any
|
||||
function Deque:popleft()
|
||||
local first = self.first
|
||||
if first > self.last then
|
||||
return nil
|
||||
end
|
||||
local value = self[first]
|
||||
self[first] = nil -- to allow garbage collection
|
||||
self.first = first + 1
|
||||
return value
|
||||
end
|
||||
|
||||
---pops from the right of the deque
|
||||
---@return any
|
||||
function Deque:popright()
|
||||
local last = self.last
|
||||
if self.first > last then
|
||||
return nil
|
||||
end
|
||||
local value = self[last]
|
||||
self[last] = nil -- to allow garbage collection
|
||||
self.last = last - 1
|
||||
return value
|
||||
end
|
||||
|
||||
---checks if the deque is empty
|
||||
---@return boolean
|
||||
function Deque:is_empty()
|
||||
return self:len() == 0
|
||||
end
|
||||
|
||||
---returns the number of elements of the deque
|
||||
---@return number
|
||||
function Deque:len()
|
||||
return self.last - self.first + 1
|
||||
end
|
||||
|
||||
---returns and iterator of the indices and values starting from the left
|
||||
---@return function
|
||||
function Deque:ipairs_left()
|
||||
local i = self.first
|
||||
|
||||
return function()
|
||||
local res = self[i]
|
||||
local idx = i
|
||||
|
||||
if res then
|
||||
i = i + 1
|
||||
|
||||
return idx, res
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---returns and iterator of the indices and values starting from the right
|
||||
---@return function
|
||||
function Deque:ipairs_right()
|
||||
local i = self.last
|
||||
|
||||
return function()
|
||||
local res = self[i]
|
||||
local idx = i
|
||||
|
||||
if res then
|
||||
i = i - 1 -- advance the iterator before we return
|
||||
|
||||
return idx, res
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---removes all values from the deque
|
||||
---@return nil
|
||||
function Deque:clear()
|
||||
for i, _ in self:ipairs_left() do
|
||||
self[i] = nil
|
||||
end
|
||||
self.first = 0
|
||||
self.last = -1
|
||||
end
|
||||
|
||||
M.Deque = Deque
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
local a = require "plenary.async_lib.async"
|
||||
local util = require "plenary.async_lib.util"
|
||||
|
||||
local M = {}
|
||||
|
||||
M.describe = function(s, func)
|
||||
describe(s, util.will_block(a.future(func)))
|
||||
end
|
||||
|
||||
M.it = function(s, func)
|
||||
it(s, util.will_block(a.future(func)))
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
local a = require "plenary.async_lib.async"
|
||||
local await = a.await
|
||||
local async = a.async
|
||||
local co = coroutine
|
||||
local Deque = require("plenary.async_lib.structs").Deque
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
---Sleep for milliseconds
|
||||
---@param ms number
|
||||
M.sleep = a.wrap(function(ms, callback)
|
||||
local timer = uv.new_timer()
|
||||
uv.timer_start(timer, ms, 0, function()
|
||||
uv.timer_stop(timer)
|
||||
uv.close(timer)
|
||||
callback()
|
||||
end)
|
||||
end, 2)
|
||||
|
||||
---Takes a future and a millisecond as the timeout.
|
||||
---If the time is reached and the future hasn't completed yet, it will short circuit the future
|
||||
---NOTE: the future will still be running in libuv, we are just not waiting for it to complete
|
||||
---thats why you should call this on a leaf future only to avoid unexpected results
|
||||
---@param future Future
|
||||
---@param ms number
|
||||
M.timeout = a.wrap(function(future, ms, callback)
|
||||
-- make sure that the callback isn't called twice, or else the coroutine can be dead
|
||||
local done = false
|
||||
|
||||
local timeout_callback = function(...)
|
||||
if not done then
|
||||
done = true
|
||||
callback(false, ...) -- false because it has run normally
|
||||
end
|
||||
end
|
||||
|
||||
vim.defer_fn(function()
|
||||
if not done then
|
||||
done = true
|
||||
callback(true) -- true because it has timed out
|
||||
end
|
||||
end, ms)
|
||||
|
||||
a.run(future, timeout_callback)
|
||||
end, 3)
|
||||
|
||||
---create an async function timer
|
||||
---@param ms number
|
||||
M.timer = function(ms)
|
||||
return async(function()
|
||||
await(M.sleep(ms))
|
||||
end)
|
||||
end
|
||||
|
||||
---id function that can be awaited
|
||||
---@param nil ...
|
||||
---@return ...
|
||||
M.id = async(function(...)
|
||||
return ...
|
||||
end)
|
||||
|
||||
---Running this function will yield now and do nothing else
|
||||
M.yield_now = async(function()
|
||||
await(M.id())
|
||||
end)
|
||||
|
||||
local Condvar = {}
|
||||
Condvar.__index = Condvar
|
||||
|
||||
---@class Condvar
|
||||
---@return Condvar
|
||||
function Condvar.new()
|
||||
return setmetatable({ handles = {} }, Condvar)
|
||||
end
|
||||
|
||||
---`blocks` the thread until a notification is received
|
||||
Condvar.wait = a.wrap(function(self, callback)
|
||||
-- not calling the callback will block the coroutine
|
||||
table.insert(self.handles, callback)
|
||||
end, 2)
|
||||
|
||||
---notify everyone that is waiting on this Condvar
|
||||
function Condvar:notify_all()
|
||||
if #self.handles == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
for i, callback in ipairs(self.handles) do
|
||||
callback()
|
||||
self.handles[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
---notify randomly one person that is waiting on this Condvar
|
||||
function Condvar:notify_one()
|
||||
if #self.handles == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local idx = math.random(#self.handles)
|
||||
self.handles[idx]()
|
||||
table.remove(self.handles, idx)
|
||||
end
|
||||
|
||||
M.Condvar = Condvar
|
||||
|
||||
local Semaphore = {}
|
||||
Semaphore.__index = Semaphore
|
||||
|
||||
---@class Semaphore
|
||||
---@param initial_permits number: the number of permits that it can give out
|
||||
---@return Semaphore
|
||||
function Semaphore.new(initial_permits)
|
||||
vim.validate {
|
||||
initial_permits = {
|
||||
initial_permits,
|
||||
function(n)
|
||||
return n > 0
|
||||
end,
|
||||
"number greater than 0",
|
||||
},
|
||||
}
|
||||
|
||||
return setmetatable({ permits = initial_permits, handles = {} }, Semaphore)
|
||||
end
|
||||
|
||||
---async function, blocks until a permit can be acquired
|
||||
---example:
|
||||
---local semaphore = Semaphore.new(1024)
|
||||
---local permit = await(semaphore:acquire())
|
||||
---permit:forget()
|
||||
---when a permit can be acquired returns it
|
||||
---call permit:forget() to forget the permit
|
||||
Semaphore.acquire = a.wrap(function(self, callback)
|
||||
if self.permits > 0 then
|
||||
self.permits = self.permits - 1
|
||||
else
|
||||
table.insert(self.handles, callback)
|
||||
return
|
||||
end
|
||||
|
||||
local permit = {}
|
||||
|
||||
permit.forget = function(self_permit)
|
||||
self.permits = self.permits + 1
|
||||
|
||||
if self.permits > 0 and #self.handles > 0 then
|
||||
self.permits = self.permits - 1
|
||||
local callback = table.remove(self.handles)
|
||||
callback(self_permit)
|
||||
end
|
||||
end
|
||||
|
||||
callback(permit)
|
||||
end, 2)
|
||||
|
||||
M.Semaphore = Semaphore
|
||||
|
||||
M.channel = {}
|
||||
|
||||
---Creates a oneshot channel
|
||||
---returns a sender and receiver function
|
||||
---the sender is not async while the receiver is
|
||||
---@return function, function
|
||||
M.channel.oneshot = function()
|
||||
local val = nil
|
||||
local saved_callback = nil
|
||||
local sent = false
|
||||
local received = false
|
||||
|
||||
--- sender is not async
|
||||
--- sends a value
|
||||
local sender = function(...)
|
||||
if sent then
|
||||
error "Oneshot channel can only send once"
|
||||
end
|
||||
|
||||
sent = true
|
||||
|
||||
local args = { ... }
|
||||
|
||||
if saved_callback then
|
||||
saved_callback(unpack(val or args))
|
||||
else
|
||||
val = args
|
||||
end
|
||||
end
|
||||
|
||||
--- receiver is async
|
||||
--- blocks until a value is received
|
||||
local receiver = a.wrap(function(callback)
|
||||
if received then
|
||||
error "Oneshot channel can only send one value!"
|
||||
end
|
||||
|
||||
if val then
|
||||
received = true
|
||||
callback(unpack(val))
|
||||
else
|
||||
saved_callback = callback
|
||||
end
|
||||
end, 1)
|
||||
|
||||
return sender, receiver
|
||||
end
|
||||
|
||||
---A counter channel.
|
||||
---Basically a channel that you want to use only to notify and not to send any actual values.
|
||||
---@return function: sender
|
||||
---@return function: receiver
|
||||
M.channel.counter = function()
|
||||
local counter = 0
|
||||
local condvar = Condvar.new()
|
||||
|
||||
local Sender = {}
|
||||
|
||||
function Sender:send()
|
||||
counter = counter + 1
|
||||
condvar:notify_all()
|
||||
end
|
||||
|
||||
local Receiver = {}
|
||||
|
||||
Receiver.recv = async(function()
|
||||
if counter == 0 then
|
||||
await(condvar:wait())
|
||||
end
|
||||
counter = counter - 1
|
||||
end)
|
||||
|
||||
Receiver.last = async(function()
|
||||
if counter == 0 then
|
||||
await(condvar:wait())
|
||||
end
|
||||
counter = 0
|
||||
end)
|
||||
|
||||
return Sender, Receiver
|
||||
end
|
||||
|
||||
---A multiple producer single consumer channel
|
||||
---@return table
|
||||
---@return table
|
||||
M.channel.mpsc = function()
|
||||
local deque = Deque.new()
|
||||
local condvar = Condvar.new()
|
||||
|
||||
local Sender = {}
|
||||
|
||||
function Sender.send(...)
|
||||
deque:pushleft { ... }
|
||||
condvar:notify_all()
|
||||
end
|
||||
|
||||
local Receiver = {}
|
||||
|
||||
Receiver.recv = async(function()
|
||||
if deque:is_empty() then
|
||||
await(condvar:wait())
|
||||
end
|
||||
return unpack(deque:popright())
|
||||
end)
|
||||
|
||||
Receiver.last = async(function()
|
||||
if deque:is_empty() then
|
||||
await(condvar:wait())
|
||||
end
|
||||
local val = deque:popright()
|
||||
deque:clear()
|
||||
return unpack(val)
|
||||
end)
|
||||
|
||||
return Sender, Receiver
|
||||
end
|
||||
|
||||
local pcall_wrap = function(func)
|
||||
return function(...)
|
||||
return pcall(func, ...)
|
||||
end
|
||||
end
|
||||
|
||||
---Makes a future protected. It is like pcall but for futures.
|
||||
---Only works for non-leaf futures
|
||||
M.protected_non_leaf = async(function(future)
|
||||
return await(pcall_wrap(future))
|
||||
end)
|
||||
|
||||
---Makes a future protected. It is like pcall but for futures.
|
||||
---@param future Future
|
||||
---@return Future
|
||||
M.protected = async(function(future)
|
||||
local tx, rx = M.channel.oneshot()
|
||||
|
||||
stat, ret = pcall(future, tx)
|
||||
|
||||
if stat == true then
|
||||
return stat, await(rx())
|
||||
else
|
||||
return stat, ret
|
||||
end
|
||||
end)
|
||||
|
||||
---This will COMPLETELY block neovim
|
||||
---please just use a.run unless you have a very special usecase
|
||||
---for example, in plenary test_harness you must use this
|
||||
---@param future Future
|
||||
---@param timeout number: Stop blocking if the timeout was surpassed. Default 2000.
|
||||
M.block_on = function(future, timeout)
|
||||
future = M.protected(future)
|
||||
|
||||
local stat, ret
|
||||
a.run(future, function(_stat, ...)
|
||||
stat = _stat
|
||||
ret = { ... }
|
||||
end)
|
||||
|
||||
local function check()
|
||||
if stat == false then
|
||||
error("Blocking on future failed " .. unpack(ret))
|
||||
end
|
||||
return stat == true
|
||||
end
|
||||
|
||||
if not vim.wait(timeout or 2000, check, 20, false) then
|
||||
error "Blocking on future timed out or was interrupted"
|
||||
end
|
||||
|
||||
return unpack(ret)
|
||||
end
|
||||
|
||||
---Returns a new future that WILL BLOCK
|
||||
---@param future Future
|
||||
---@return Future
|
||||
M.will_block = async(function(future)
|
||||
return M.block_on(future)
|
||||
end)
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
local a = require "plenary.async_lib.async"
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
local function add(name, argc)
|
||||
local success, ret = pcall(a.wrap, uv[name], argc)
|
||||
|
||||
if not success then
|
||||
error("Failed to add function with name " .. name)
|
||||
end
|
||||
|
||||
M[name] = ret
|
||||
end
|
||||
|
||||
add("close", 4) -- close a handle
|
||||
|
||||
-- filesystem operations
|
||||
add("fs_open", 4)
|
||||
add("fs_read", 4)
|
||||
add("fs_close", 2)
|
||||
add("fs_unlink", 2)
|
||||
add("fs_write", 4)
|
||||
add("fs_mkdir", 3)
|
||||
add("fs_mkdtemp", 2)
|
||||
-- 'fs_mkstemp',
|
||||
add("fs_rmdir", 2)
|
||||
add("fs_scandir", 2)
|
||||
add("fs_stat", 2)
|
||||
add("fs_fstat", 2)
|
||||
add("fs_lstat", 2)
|
||||
add("fs_rename", 3)
|
||||
add("fs_fsync", 2)
|
||||
add("fs_fdatasync", 2)
|
||||
add("fs_ftruncate", 3)
|
||||
add("fs_sendfile", 5)
|
||||
add("fs_access", 3)
|
||||
add("fs_chmod", 3)
|
||||
add("fs_fchmod", 3)
|
||||
add("fs_utime", 4)
|
||||
add("fs_futime", 4)
|
||||
-- 'fs_lutime',
|
||||
add("fs_link", 3)
|
||||
add("fs_symlink", 4)
|
||||
add("fs_readlink", 2)
|
||||
add("fs_realpath", 2)
|
||||
add("fs_chown", 4)
|
||||
add("fs_fchown", 4)
|
||||
-- 'fs_lchown',
|
||||
add("fs_copyfile", 4)
|
||||
-- add('fs_opendir', 3) -- TODO: fix this one
|
||||
add("fs_readdir", 2)
|
||||
add("fs_closedir", 2)
|
||||
-- 'fs_statfs',
|
||||
|
||||
-- stream
|
||||
add("shutdown", 2)
|
||||
add("listen", 3)
|
||||
-- add('read_start', 2) -- do not do this one, the callback is made multiple times
|
||||
add("write", 3)
|
||||
add("write2", 4)
|
||||
add("shutdown", 2)
|
||||
|
||||
-- tcp
|
||||
add("tcp_connect", 4)
|
||||
-- 'tcp_close_reset',
|
||||
|
||||
-- pipe
|
||||
add("pipe_connect", 3)
|
||||
|
||||
-- udp
|
||||
add("udp_send", 5)
|
||||
add("udp_recv_start", 2)
|
||||
|
||||
-- fs event (wip make into async await event)
|
||||
-- fs poll event (wip make into async await event)
|
||||
|
||||
-- dns
|
||||
add("getaddrinfo", 4)
|
||||
add("getnameinfo", 2)
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
local stat = require "plenary.benchmark.stat"
|
||||
|
||||
local get_stats = function(results)
|
||||
local ret = {}
|
||||
|
||||
ret.max, ret.min = stat.maxmin(results)
|
||||
ret.mean = stat.mean(results)
|
||||
ret.median = stat.median(results)
|
||||
ret.std = stat.std_dev(results)
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
local get_output = function(index, res, runs)
|
||||
-- divine with a sutable one / 1e3, 1e6, 1e9
|
||||
local time_types = { "ns", "μs", "ms" }
|
||||
|
||||
local get_leading = function(time)
|
||||
time = math.floor(time)
|
||||
local count = 0
|
||||
repeat
|
||||
time = math.floor(time / 10)
|
||||
count = count + 1
|
||||
until time <= 0
|
||||
return count
|
||||
end
|
||||
|
||||
local get_best_fmt = function(time)
|
||||
for _, v in ipairs(time_types) do
|
||||
if math.abs(time) < 1000.0 then
|
||||
return string.format("%s%3.1f %s", string.rep(" ", 3 - get_leading(time)), time, v)
|
||||
end
|
||||
time = time / 1000.0
|
||||
end
|
||||
return string.format("%.1f %s", time, "s")
|
||||
end
|
||||
|
||||
return string.format(
|
||||
"Benchmark #%d: '%s'\n Time(mean ± σ): %s ± %s\n Range(min … max): %s … %s %d runs\n",
|
||||
index,
|
||||
res.name,
|
||||
get_best_fmt(res.stats.mean),
|
||||
get_best_fmt(res.stats.std),
|
||||
get_best_fmt(res.stats.min),
|
||||
get_best_fmt(res.stats.max),
|
||||
runs
|
||||
)
|
||||
end
|
||||
|
||||
local get_summary = function(res)
|
||||
if #res == 1 then
|
||||
return ""
|
||||
end
|
||||
|
||||
local fastest_mean = math.huge
|
||||
local fastest_index = 1
|
||||
for i, benchmark in ipairs(res) do
|
||||
if benchmark.stats.mean < fastest_mean then
|
||||
fastest_mean = benchmark.stats.mean
|
||||
fastest_index = i
|
||||
end
|
||||
end
|
||||
|
||||
if fastest_mean == math.huge then
|
||||
return ""
|
||||
end
|
||||
|
||||
local output = {}
|
||||
local fastest = res[fastest_index].stats
|
||||
for i, benchmark in ipairs(res) do
|
||||
if i ~= fastest_index then
|
||||
local result = benchmark.stats
|
||||
local ratio = result.mean / fastest.mean
|
||||
|
||||
-- // https://en.wikipedia.org/wiki/Propagation_of_uncertainty#Example_formulas
|
||||
-- // Covariance asssumed to be 0, i.e. variables are assumed to be independent
|
||||
local ratio_std = ratio
|
||||
* math.sqrt(math.pow(result.std / result.mean, 2) + math.pow(fastest.std / fastest.mean, 2))
|
||||
|
||||
table.insert(output, string.format(" %.1f ± %.1f times faster than '%s'\n", ratio, ratio_std, benchmark.name))
|
||||
end
|
||||
end
|
||||
|
||||
return string.format("Summary\n '%s' ran\n%s", res[fastest_index].name, table.concat(output, ""))
|
||||
end
|
||||
|
||||
---@class benchmark_run_opts
|
||||
---@field warmup number @number of initial runs before starting to track time.
|
||||
---@field runs number @number of runs to make
|
||||
---@field fun table<array<string, function>> @functions to execute
|
||||
|
||||
---Benchmark a function
|
||||
---@param name string @benchmark name
|
||||
---@param opts benchmark_run_opts
|
||||
local bench = function(name, opts)
|
||||
vim.validate {
|
||||
opts = { opts, "table" },
|
||||
fun = { opts.fun, "table" },
|
||||
}
|
||||
opts.warmup = vim.F.if_nil(opts.warmup, 3)
|
||||
opts.runs = vim.F.if_nil(opts.runs, 5)
|
||||
|
||||
opts.fun = type(opts.fun) == "function" and { opts.fun } or opts.fun
|
||||
local output = { string.format("Benchmark Group: '%s' -----------------------\n", name) }
|
||||
local res = {}
|
||||
for i, fun in ipairs(opts.fun) do
|
||||
res[i] = { name = fun[1], results = {} }
|
||||
for _ = 1, opts.warmup do
|
||||
fun[2]()
|
||||
end
|
||||
for j = 1, opts.runs do
|
||||
local start = vim.loop.hrtime()
|
||||
fun[2]()
|
||||
res[i].results[j] = vim.loop.hrtime() - start
|
||||
end
|
||||
res[i].stats = get_stats(res[i].results)
|
||||
table.insert(output, get_output(i, res[i], opts.runs))
|
||||
end
|
||||
|
||||
print(string.format("%s\n%s", table.concat(output, ""), get_summary(res)))
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
return bench
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
local stat = {}
|
||||
|
||||
---Calculate mean
|
||||
---@param t number[] @double
|
||||
---@return number @double
|
||||
stat.mean = function(t)
|
||||
local sum = 0
|
||||
local count = 0
|
||||
|
||||
for _, v in pairs(t) do
|
||||
if type(v) == "number" then
|
||||
sum = sum + v
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
|
||||
return (sum / count)
|
||||
end
|
||||
|
||||
-- Get the median of a table.
|
||||
---@param t number[]
|
||||
---@return number
|
||||
stat.median = function(t)
|
||||
local temp = {}
|
||||
|
||||
-- deep copy table so that when we sort it, the original is unchanged
|
||||
-- also weed out any non numbers
|
||||
for _, v in pairs(t) do
|
||||
if type(v) == "number" then
|
||||
table.insert(temp, v)
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(temp)
|
||||
|
||||
-- If we have an even number of table elements or odd.
|
||||
if math.fmod(#temp, 2) == 0 then
|
||||
-- return mean value of middle two elements
|
||||
return (temp[#temp / 2] + temp[(#temp / 2) + 1]) / 2
|
||||
else
|
||||
-- return middle element
|
||||
return temp[math.ceil(#temp / 2)]
|
||||
end
|
||||
end
|
||||
|
||||
--- Get the standard deviation of a table
|
||||
---@param t number[]
|
||||
stat.std_dev = function(t)
|
||||
local m, vm, result
|
||||
local sum = 0
|
||||
local count = 0
|
||||
|
||||
m = stat.mean(t)
|
||||
|
||||
for _, v in pairs(t) do
|
||||
if type(v) == "number" then
|
||||
vm = v - m
|
||||
sum = sum + (vm * vm)
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
|
||||
result = math.sqrt(sum / (count - 1))
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---Get the max and min for a table
|
||||
---@param t number[]
|
||||
---@return number
|
||||
---@return number
|
||||
stat.maxmin = function(t)
|
||||
local max = -math.huge
|
||||
local min = math.huge
|
||||
|
||||
for _, v in pairs(t) do
|
||||
if type(v) == "number" then
|
||||
max = math.max(max, v)
|
||||
min = math.min(min, v)
|
||||
end
|
||||
end
|
||||
|
||||
return max, min
|
||||
end
|
||||
|
||||
return stat
|
||||
339
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/bit.lua
Normal file
339
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/bit.lua
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
-- Shortcircuit to returning bit if it already exists
|
||||
if bit then return bit end
|
||||
|
||||
--[[
|
||||
|
||||
Credit: https://github.com/davidm/lua-bit-numberlua/blob/master/lmod/bit/numberlua.lua
|
||||
|
||||
LUA MODULE
|
||||
|
||||
bit.numberlua - Bitwise operations implemented in pure Lua as numbers,
|
||||
with Lua 5.2 'bit32' and (LuaJIT) LuaBitOp 'bit' compatibility interfaces.
|
||||
|
||||
SYNOPSIS
|
||||
|
||||
local bit = require 'bit.numberlua'
|
||||
print(bit.band(0xff00ff00, 0x00ff00ff)) --> 0xffffffff
|
||||
|
||||
-- Interface providing strong (LuaJIT) LuaBitOp 'bit' compatibility
|
||||
local bit = require 'plenary.bit'
|
||||
assert(bit.tobit(0xffffffff) == -1)
|
||||
|
||||
REMOVED!
|
||||
-- Interface providing strong Lua 5.2 'bit32' compatibility
|
||||
local bit32 = require 'bit.numberlua'.bit32
|
||||
assert(bit32.band(-1) == 0xffffffff)
|
||||
|
||||
|
||||
DESCRIPTION
|
||||
|
||||
This library implements bitwise operations entirely in Lua.
|
||||
This module is typically intended if for some reasons you don't want
|
||||
to or cannot install a popular C based bit library like BitOp 'bit' [1]
|
||||
(which comes pre-installed with LuaJIT) or 'bit32' (which comes
|
||||
pre-installed with Lua 5.2) but want a similar interface.
|
||||
|
||||
This modules represents bit arrays as non-negative Lua numbers. [1]
|
||||
It can represent 32-bit bit arrays when Lua is compiled
|
||||
with lua_Number as double-precision IEEE 754 floating point.
|
||||
|
||||
The module is nearly the most efficient it can be but may be a few times
|
||||
slower than the C based bit libraries and is orders or magnitude
|
||||
slower than LuaJIT bit operations, which compile to native code. Therefore,
|
||||
this library is inferior in performane to the other modules.
|
||||
|
||||
The `xor` function in this module is based partly on Roberto Ierusalimschy's
|
||||
post in http://lua-users.org/lists/lua-l/2002-09/msg00134.html .
|
||||
|
||||
The included BIT.bit32 and BIT.bit sublibraries aims to provide 100%
|
||||
compatibility with the Lua 5.2 "bit32" and (LuaJIT) LuaBitOp "bit" library.
|
||||
This compatbility is at the cost of some efficiency since inputted
|
||||
numbers are normalized and more general forms (e.g. multi-argument
|
||||
bitwise operators) are supported.
|
||||
|
||||
STATUS
|
||||
|
||||
WARNING: Not all corner cases have been tested and documented.
|
||||
Some attempt was made to make these similar to the Lua 5.2 [2]
|
||||
and LuaJit BitOp [3] libraries, but this is not fully tested and there
|
||||
are currently some differences. Addressing these differences may
|
||||
be improved in the future but it is not yet fully determined how to
|
||||
resolve these differences.
|
||||
|
||||
The BIT.bit32 library passes the Lua 5.2 test suite (bitwise.lua)
|
||||
http://www.lua.org/tests/5.2/ . The BIT.bit library passes the LuaBitOp
|
||||
test suite (bittest.lua). However, these have not been tested on
|
||||
platforms with Lua compiled with 32-bit integer numbers.
|
||||
|
||||
API
|
||||
|
||||
Module's return
|
||||
|
||||
This table contains functions that aim to provide 100% compatibility
|
||||
with the LuaBitOp "bit" library (from LuaJIT).
|
||||
|
||||
bit.tobit(x) --> y
|
||||
bit.tohex(x [,n]) --> y
|
||||
bit.bnot(x) --> y
|
||||
bit.bor(x1 [,x2...]) --> y
|
||||
bit.band(x1 [,x2...]) --> y
|
||||
bit.bxor(x1 [,x2...]) --> y
|
||||
bit.lshift(x, n) --> y
|
||||
bit.rshift(x, n) --> y
|
||||
bit.arshift(x, n) --> y
|
||||
bit.rol(x, n) --> y
|
||||
bit.ror(x, n) --> y
|
||||
bit.bswap(x) --> y
|
||||
|
||||
DEPENDENCIES
|
||||
|
||||
None (other than Lua 5.1 or 5.2).
|
||||
|
||||
REFERENCES
|
||||
|
||||
[1] http://lua-users.org/wiki/FloatingPoint
|
||||
[2] http://www.lua.org/manual/5.2/
|
||||
[3] http://bitop.luajit.org/
|
||||
|
||||
LICENSE
|
||||
|
||||
(c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT).
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
(end license)
|
||||
|
||||
Some modifications by plenary team.
|
||||
|
||||
--]]
|
||||
|
||||
local M = {_TYPE='module', _NAME='bit.numberlua', _VERSION='0.3.1.20120131'}
|
||||
|
||||
local floor = math.floor
|
||||
|
||||
local MOD = 2^32
|
||||
local MODM = MOD-1
|
||||
|
||||
local function memoize(f)
|
||||
local mt = {}
|
||||
local t = setmetatable({}, mt)
|
||||
function mt:__index(k)
|
||||
local v = f(k); t[k] = v
|
||||
return v
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local function make_bitop_uncached(t, m)
|
||||
local function bitop(a, b)
|
||||
local res,p = 0,1
|
||||
while a ~= 0 and b ~= 0 do
|
||||
local am, bm = a%m, b%m
|
||||
res = res + t[am][bm]*p
|
||||
a = (a - am) / m
|
||||
b = (b - bm) / m
|
||||
p = p*m
|
||||
end
|
||||
res = res + (a+b)*p
|
||||
return res
|
||||
end
|
||||
return bitop
|
||||
end
|
||||
|
||||
local function make_bitop(t)
|
||||
local op1 = make_bitop_uncached(t,2^1)
|
||||
local op2 = memoize(function(a)
|
||||
return memoize(function(b)
|
||||
return op1(a, b)
|
||||
end)
|
||||
end)
|
||||
return make_bitop_uncached(op2, 2^(t.n or 1))
|
||||
end
|
||||
|
||||
-- ok? probably not if running on a 32-bit int Lua number type platform
|
||||
function M.tobit(x)
|
||||
return x % 2^32
|
||||
end
|
||||
|
||||
M.bxor = make_bitop {[0]={[0]=0,[1]=1},[1]={[0]=1,[1]=0}, n=4}
|
||||
local bxor = M.bxor
|
||||
|
||||
function M.bnot(a) return MODM - a end
|
||||
local bnot = M.bnot
|
||||
|
||||
function M.band(a,b) return ((a+b) - bxor(a,b))/2 end
|
||||
local band = M.band
|
||||
|
||||
function M.bor(a,b) return MODM - band(MODM - a, MODM - b) end
|
||||
local bor = M.bor
|
||||
|
||||
local lshift, rshift -- forward declare
|
||||
|
||||
function M.rshift(a,disp) -- Lua5.2 insipred
|
||||
if disp < 0 then return lshift(a,-disp) end
|
||||
return floor(a % 2^32 / 2^disp)
|
||||
end
|
||||
rshift = M.rshift
|
||||
|
||||
function M.lshift(a,disp) -- Lua5.2 inspired
|
||||
if disp < 0 then return rshift(a,-disp) end
|
||||
return (a * 2^disp) % 2^32
|
||||
end
|
||||
lshift = M.lshift
|
||||
|
||||
function M.tohex(x, n) -- BitOp style
|
||||
n = n or 8
|
||||
local up
|
||||
if n <= 0 then
|
||||
if n == 0 then return '' end
|
||||
up = true
|
||||
n = - n
|
||||
end
|
||||
x = band(x, 16^n-1)
|
||||
return ('%0'..n..(up and 'X' or 'x')):format(x)
|
||||
end
|
||||
local tohex = M.tohex
|
||||
|
||||
function M.extract(n, field, width) -- Lua5.2 inspired
|
||||
width = width or 1
|
||||
return band(rshift(n, field), 2^width-1)
|
||||
end
|
||||
|
||||
function M.replace(n, v, field, width) -- Lua5.2 inspired
|
||||
width = width or 1
|
||||
local mask1 = 2^width-1
|
||||
v = band(v, mask1) -- required by spec?
|
||||
local mask = bnot(lshift(mask1, field))
|
||||
return band(n, mask) + lshift(v, field)
|
||||
end
|
||||
|
||||
function M.bswap(x) -- BitOp style
|
||||
local a = band(x, 0xff); x = rshift(x, 8)
|
||||
local b = band(x, 0xff); x = rshift(x, 8)
|
||||
local c = band(x, 0xff); x = rshift(x, 8)
|
||||
local d = band(x, 0xff)
|
||||
return lshift(lshift(lshift(a, 8) + b, 8) + c, 8) + d
|
||||
end
|
||||
local bswap = M.bswap
|
||||
|
||||
function M.rrotate(x, disp) -- Lua5.2 inspired
|
||||
disp = disp % 32
|
||||
local low = band(x, 2^disp-1)
|
||||
return rshift(x, disp) + lshift(low, 32-disp)
|
||||
end
|
||||
local rrotate = M.rrotate
|
||||
|
||||
function M.lrotate(x, disp) -- Lua5.2 inspired
|
||||
return rrotate(x, -disp)
|
||||
end
|
||||
local lrotate = M.lrotate
|
||||
|
||||
M.rol = M.lrotate -- LuaOp inspired
|
||||
M.ror = M.rrotate -- LuaOp insipred
|
||||
|
||||
|
||||
function M.arshift(x, disp) -- Lua5.2 inspired
|
||||
local z = rshift(x, disp)
|
||||
if x >= 0x80000000 then z = z + lshift(2^disp-1, 32-disp) end
|
||||
return z
|
||||
end
|
||||
local arshift = M.arshift
|
||||
|
||||
function M.btest(x, y) -- Lua5.2 inspired
|
||||
return band(x, y) ~= 0
|
||||
end
|
||||
|
||||
--
|
||||
-- Start LuaBitOp "bit" compat section.
|
||||
--
|
||||
|
||||
M.bit = {} -- LuaBitOp "bit" compatibility
|
||||
|
||||
function M.bit.tobit(x)
|
||||
x = x % MOD
|
||||
if x >= 0x80000000 then x = x - MOD end
|
||||
return x
|
||||
end
|
||||
local bit_tobit = M.bit.tobit
|
||||
|
||||
function M.bit.tohex(x, ...)
|
||||
return tohex(x % MOD, ...)
|
||||
end
|
||||
|
||||
function M.bit.bnot(x)
|
||||
return bit_tobit(bnot(x % MOD))
|
||||
end
|
||||
|
||||
local function bit_bor(a, b, c, ...)
|
||||
if c then
|
||||
return bit_bor(bit_bor(a, b), c, ...)
|
||||
elseif b then
|
||||
return bit_tobit(bor(a % MOD, b % MOD))
|
||||
else
|
||||
return bit_tobit(a)
|
||||
end
|
||||
end
|
||||
M.bit.bor = bit_bor
|
||||
|
||||
local function bit_band(a, b, c, ...)
|
||||
if c then
|
||||
return bit_band(bit_band(a, b), c, ...)
|
||||
elseif b then
|
||||
return bit_tobit(band(a % MOD, b % MOD))
|
||||
else
|
||||
return bit_tobit(a)
|
||||
end
|
||||
end
|
||||
M.bit.band = bit_band
|
||||
|
||||
local function bit_bxor(a, b, c, ...)
|
||||
if c then
|
||||
return bit_bxor(bit_bxor(a, b), c, ...)
|
||||
elseif b then
|
||||
return bit_tobit(bxor(a % MOD, b % MOD))
|
||||
else
|
||||
return bit_tobit(a)
|
||||
end
|
||||
end
|
||||
M.bit.bxor = bit_bxor
|
||||
|
||||
function M.bit.lshift(x, n)
|
||||
return bit_tobit(lshift(x % MOD, n % 32))
|
||||
end
|
||||
|
||||
function M.bit.rshift(x, n)
|
||||
return bit_tobit(rshift(x % MOD, n % 32))
|
||||
end
|
||||
|
||||
function M.bit.arshift(x, n)
|
||||
return bit_tobit(arshift(x % MOD, n % 32))
|
||||
end
|
||||
|
||||
function M.bit.rol(x, n)
|
||||
return bit_tobit(lrotate(x % MOD, n % 32))
|
||||
end
|
||||
|
||||
function M.bit.ror(x, n)
|
||||
return bit_tobit(rrotate(x % MOD, n % 32))
|
||||
end
|
||||
|
||||
function M.bit.bswap(x)
|
||||
return bit_tobit(bswap(x % MOD))
|
||||
end
|
||||
|
||||
return M.bit
|
||||
271
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/busted.lua
Normal file
271
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/busted.lua
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
local dirname = function(p)
|
||||
return vim.fn.fnamemodify(p, ":h")
|
||||
end
|
||||
|
||||
local function get_trace(element, level, msg)
|
||||
local function trimTrace(info)
|
||||
local index = info.traceback:find "\n%s*%[C]"
|
||||
info.traceback = info.traceback:sub(1, index)
|
||||
return info
|
||||
end
|
||||
level = level or 3
|
||||
|
||||
local thisdir = dirname(debug.getinfo(1, "Sl").source, ":h")
|
||||
local info = debug.getinfo(level, "Sl")
|
||||
while
|
||||
info.what == "C"
|
||||
or info.short_src:match "luassert[/\\].*%.lua$"
|
||||
or (info.source:sub(1, 1) == "@" and thisdir == dirname(info.source))
|
||||
do
|
||||
level = level + 1
|
||||
info = debug.getinfo(level, "Sl")
|
||||
end
|
||||
|
||||
info.traceback = debug.traceback("", level)
|
||||
info.message = msg
|
||||
|
||||
-- local file = busted.getFile(element)
|
||||
local file = false
|
||||
return file and file.getTrace(file.name, info) or trimTrace(info)
|
||||
end
|
||||
|
||||
local is_headless = require("plenary.nvim_meta").is_headless
|
||||
|
||||
-- We are shadowing print so people can reliably print messages
|
||||
print = function(...)
|
||||
for _, v in ipairs { ... } do
|
||||
io.stdout:write(tostring(v))
|
||||
io.stdout:write "\t"
|
||||
end
|
||||
|
||||
io.stdout:write "\r\n"
|
||||
end
|
||||
|
||||
local mod = {}
|
||||
|
||||
local results = {}
|
||||
local current_description = {}
|
||||
local current_before_each = {}
|
||||
local current_after_each = {}
|
||||
|
||||
local add_description = function(desc)
|
||||
table.insert(current_description, desc)
|
||||
|
||||
return vim.deepcopy(current_description)
|
||||
end
|
||||
|
||||
local pop_description = function()
|
||||
current_description[#current_description] = nil
|
||||
end
|
||||
|
||||
local add_new_each = function()
|
||||
current_before_each[#current_description] = {}
|
||||
current_after_each[#current_description] = {}
|
||||
end
|
||||
|
||||
local clear_last_each = function()
|
||||
current_before_each[#current_description] = nil
|
||||
current_after_each[#current_description] = nil
|
||||
end
|
||||
|
||||
local call_inner = function(desc, func)
|
||||
local desc_stack = add_description(desc)
|
||||
add_new_each()
|
||||
local ok, msg = xpcall(func, function(msg)
|
||||
-- debug.traceback
|
||||
-- return vim.inspect(get_trace(nil, 3, msg))
|
||||
local trace = get_trace(nil, 3, msg)
|
||||
return trace.message .. "\n" .. trace.traceback
|
||||
end)
|
||||
clear_last_each()
|
||||
pop_description()
|
||||
|
||||
return ok, msg, desc_stack
|
||||
end
|
||||
|
||||
local color_table = {
|
||||
yellow = 33,
|
||||
green = 32,
|
||||
red = 31,
|
||||
}
|
||||
|
||||
local color_string = function(color, str)
|
||||
if not is_headless then
|
||||
return str
|
||||
end
|
||||
|
||||
return string.format("%s[%sm%s%s[%sm", string.char(27), color_table[color] or 0, str, string.char(27), 0)
|
||||
end
|
||||
|
||||
local SUCCESS = color_string("green", "Success")
|
||||
local FAIL = color_string("red", "Fail")
|
||||
local PENDING = color_string("yellow", "Pending")
|
||||
|
||||
local HEADER = string.rep("=", 40)
|
||||
|
||||
mod.format_results = function(res)
|
||||
print ""
|
||||
print(color_string("green", "Success: "), #res.pass)
|
||||
print(color_string("red", "Failed : "), #res.fail)
|
||||
print(color_string("red", "Errors : "), #res.errs)
|
||||
print(HEADER)
|
||||
end
|
||||
|
||||
mod.describe = function(desc, func)
|
||||
results.pass = results.pass or {}
|
||||
results.fail = results.fail or {}
|
||||
results.errs = results.errs or {}
|
||||
|
||||
describe = mod.inner_describe
|
||||
local ok, msg, desc_stack = call_inner(desc, func)
|
||||
describe = mod.describe
|
||||
|
||||
if not ok then
|
||||
table.insert(results.errs, {
|
||||
descriptions = desc_stack,
|
||||
msg = msg,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
mod.inner_describe = function(desc, func)
|
||||
local ok, msg, desc_stack = call_inner(desc, func)
|
||||
|
||||
if not ok then
|
||||
table.insert(results.errs, {
|
||||
descriptions = desc_stack,
|
||||
msg = msg,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
mod.before_each = function(fn)
|
||||
table.insert(current_before_each[#current_description], fn)
|
||||
end
|
||||
|
||||
mod.after_each = function(fn)
|
||||
table.insert(current_after_each[#current_description], fn)
|
||||
end
|
||||
|
||||
mod.clear = function()
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, {})
|
||||
end
|
||||
|
||||
local indent = function(msg, spaces)
|
||||
if spaces == nil then
|
||||
spaces = 4
|
||||
end
|
||||
|
||||
local prefix = string.rep(" ", spaces)
|
||||
return prefix .. msg:gsub("\n", "\n" .. prefix)
|
||||
end
|
||||
|
||||
local run_each = function(tbl)
|
||||
for _, v in ipairs(tbl) do
|
||||
for _, w in ipairs(v) do
|
||||
if type(w) == "function" then
|
||||
w()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
mod.it = function(desc, func)
|
||||
run_each(current_before_each)
|
||||
local ok, msg, desc_stack = call_inner(desc, func)
|
||||
run_each(current_after_each)
|
||||
|
||||
local test_result = {
|
||||
descriptions = desc_stack,
|
||||
msg = nil,
|
||||
}
|
||||
|
||||
-- TODO: We should figure out how to determine whether
|
||||
-- and assert failed or whether it was an error...
|
||||
|
||||
local to_insert
|
||||
if not ok then
|
||||
to_insert = results.fail
|
||||
test_result.msg = msg
|
||||
|
||||
print(FAIL, "||", table.concat(test_result.descriptions, " "))
|
||||
print(indent(msg, 12))
|
||||
else
|
||||
to_insert = results.pass
|
||||
print(SUCCESS, "||", table.concat(test_result.descriptions, " "))
|
||||
end
|
||||
|
||||
table.insert(to_insert, test_result)
|
||||
end
|
||||
|
||||
mod.pending = function(desc, func)
|
||||
local curr_stack = vim.deepcopy(current_description)
|
||||
table.insert(curr_stack, desc)
|
||||
print(PENDING, "||", table.concat(curr_stack, " "))
|
||||
end
|
||||
|
||||
_PlenaryBustedOldAssert = _PlenaryBustedOldAssert or assert
|
||||
|
||||
describe = mod.describe
|
||||
it = mod.it
|
||||
pending = mod.pending
|
||||
before_each = mod.before_each
|
||||
after_each = mod.after_each
|
||||
clear = mod.clear
|
||||
---@type Luassert
|
||||
assert = require "luassert"
|
||||
|
||||
mod.run = function(file)
|
||||
file = file:gsub("\\", "/")
|
||||
|
||||
print("\n" .. HEADER)
|
||||
print("Testing: ", file)
|
||||
|
||||
local loaded, msg = loadfile(file)
|
||||
|
||||
if not loaded then
|
||||
print(HEADER)
|
||||
print "FAILED TO LOAD FILE"
|
||||
print(color_string("red", msg))
|
||||
print(HEADER)
|
||||
if is_headless then
|
||||
return vim.cmd "2cq"
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
coroutine.wrap(function()
|
||||
loaded()
|
||||
|
||||
-- If nothing runs (empty file without top level describe)
|
||||
if not results.pass then
|
||||
if is_headless then
|
||||
return vim.cmd "0cq"
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
mod.format_results(results)
|
||||
|
||||
if #results.errs ~= 0 then
|
||||
print("We had an unexpected error: ", vim.inspect(results.errs), vim.inspect(results))
|
||||
if is_headless then
|
||||
return vim.cmd "2cq"
|
||||
end
|
||||
elseif #results.fail > 0 then
|
||||
print "Tests Failed. Exit: 1"
|
||||
|
||||
if is_headless then
|
||||
return vim.cmd "1cq"
|
||||
end
|
||||
else
|
||||
if is_headless then
|
||||
return vim.cmd "0cq"
|
||||
end
|
||||
end
|
||||
end)()
|
||||
end
|
||||
|
||||
return mod
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
---@brief [[
|
||||
---classic
|
||||
---
|
||||
---Copyright (c) 2014, rxi
|
||||
---@brief ]]
|
||||
|
||||
---@class Object
|
||||
local Object = {}
|
||||
Object.__index = Object
|
||||
|
||||
---Does nothing.
|
||||
---You have to implement this yourself for extra functionality when initializing
|
||||
---@param self Object
|
||||
function Object:new() end
|
||||
|
||||
---Create a new class/object by extending the base Object class.
|
||||
---The extended object will have a field called `super` that will access the super class.
|
||||
---@param self Object
|
||||
---@return Object
|
||||
function Object:extend()
|
||||
local cls = {}
|
||||
for k, v in pairs(self) do
|
||||
if k:find "__" == 1 then
|
||||
cls[k] = v
|
||||
end
|
||||
end
|
||||
cls.__index = cls
|
||||
cls.super = self
|
||||
setmetatable(cls, self)
|
||||
return cls
|
||||
end
|
||||
|
||||
---Implement a mixin onto this Object.
|
||||
---@param self Object
|
||||
---@param nil ...
|
||||
function Object:implement(...)
|
||||
for _, cls in pairs { ... } do
|
||||
for k, v in pairs(cls) do
|
||||
if self[k] == nil and type(v) == "function" then
|
||||
self[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Checks if the object is an instance
|
||||
---This will start with the lowest class and loop over all the superclasses.
|
||||
---@param self Object
|
||||
---@param T Object
|
||||
---@return boolean
|
||||
function Object:is(T)
|
||||
local mt = getmetatable(self)
|
||||
while mt do
|
||||
if mt == T then
|
||||
return true
|
||||
end
|
||||
mt = getmetatable(mt)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---The default tostring implementation for an object.
|
||||
---You can override this to provide a different tostring.
|
||||
---@param self Object
|
||||
---@return string
|
||||
function Object:__tostring()
|
||||
return "Object"
|
||||
end
|
||||
|
||||
---You can call the class the initialize it without using `Object:new`.
|
||||
---@param self Object
|
||||
---@param nil ...
|
||||
---@return Object
|
||||
function Object:__call(...)
|
||||
local obj = setmetatable({}, self)
|
||||
obj:new(...)
|
||||
return obj
|
||||
end
|
||||
|
||||
return Object
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
---@brief [[
|
||||
--- This module implements python-like lists. It can be used like so:
|
||||
--- <pre>
|
||||
--- local List = require 'plenary.collections.py_list'
|
||||
--- local l = List{3, 20, 44}
|
||||
--- print(l) -- [3, 20, 44]
|
||||
--- </pre>
|
||||
---@brief ]]
|
||||
local List = {}
|
||||
|
||||
---@class List @The base class for all list objects
|
||||
|
||||
---List constructor. Can be used in higher order functions
|
||||
---@param tbl table: A list-like table containing the initial elements of the list
|
||||
---@return List: A new list object
|
||||
function List.new(tbl)
|
||||
if type(tbl) == "table" then
|
||||
local len = #tbl
|
||||
local obj = setmetatable(tbl, List)
|
||||
obj._len = len
|
||||
return obj
|
||||
end
|
||||
error "List constructor must be called with table argument"
|
||||
end
|
||||
|
||||
--- Checks whether the argument is a List object
|
||||
--- @param tbl table: The object to test
|
||||
--- @return boolean: Whether tbl is an instance of List
|
||||
function List.is_list(tbl)
|
||||
local meta = getmetatable(tbl) or {}
|
||||
return meta == List
|
||||
end
|
||||
|
||||
function List:__index(key)
|
||||
if self ~= List then
|
||||
local field = List[key]
|
||||
if field then
|
||||
return field
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: Similar to python, use [...] if the table references itself --
|
||||
function List:__tostring()
|
||||
local elements = self:join ", "
|
||||
return "[" .. elements .. "]"
|
||||
end
|
||||
|
||||
function List:__eq(other)
|
||||
if #self ~= #other then
|
||||
return false
|
||||
end
|
||||
for i = 1, #self do
|
||||
if self[i] ~= other[i] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function List:__mul(other)
|
||||
local result = List.new {}
|
||||
for i = 1, other do
|
||||
result[i] = self
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function List:__len()
|
||||
return self._len
|
||||
end
|
||||
|
||||
function List:__concat(other)
|
||||
return self:concat(other)
|
||||
end
|
||||
|
||||
--- Pushes the element to the end of the list
|
||||
--- @param other any: The object to append
|
||||
--- @see List.pop
|
||||
function List:push(other)
|
||||
self[#self + 1] = other
|
||||
self._len = self._len + 1
|
||||
end
|
||||
|
||||
--- Pops the last element off the list and returns it
|
||||
--- @return any: The (previously) last element from the list
|
||||
--- @see List.push
|
||||
function List:pop()
|
||||
local result = table.remove(self)
|
||||
self._len = self._len - 1
|
||||
return result
|
||||
end
|
||||
|
||||
--- Inserts other into the specified idx
|
||||
--- @param idx number: The index that other will be inserted to
|
||||
--- @param other any: The element to insert
|
||||
--- @see List.remove
|
||||
function List:insert(idx, other)
|
||||
table.insert(self, idx, other)
|
||||
self._len = self._len + 1
|
||||
end
|
||||
|
||||
--- Removes the element at index idx and returns it
|
||||
--- @param idx number: The index of the element to remove
|
||||
--- @return any: The element previously at index idx
|
||||
--- @see List.insert
|
||||
function List:remove(idx)
|
||||
self._len = self._len - 1
|
||||
return table.remove(self, idx)
|
||||
end
|
||||
|
||||
--- Can be used to compare elements with any list-like table. It only checks for
|
||||
--- shallow equality
|
||||
--- @param other any: The element to test for
|
||||
--- @return boolean: True if other is a list object and all it's elements are equal
|
||||
--- @see List.deep_equal
|
||||
function List:equal(other)
|
||||
return self:__eq(other)
|
||||
end
|
||||
|
||||
--- Checks for deep equality between lists. This uses vim.deep_equal for testing
|
||||
--- @param other any: The element to test for
|
||||
--- @return boolean: True if all elements and their children are equal
|
||||
--- @see List.equal
|
||||
--- @see vim.deep_equal
|
||||
function List:deep_equal(other)
|
||||
return vim.deep_equal(self, other)
|
||||
end
|
||||
|
||||
--- Returns a copy of the list with elements between a and b, inclusive
|
||||
--- <pre>
|
||||
--- local list = List{1, 2, 3, 4}
|
||||
--- local slice = list:slice(2, 3)
|
||||
--- print(slice) -- [2, 3]
|
||||
--- </pre>
|
||||
--- @param a number: The low end of the slice
|
||||
--- @param b number: The high end of the slice
|
||||
--- @return List: A list with elements between a and b
|
||||
function List:slice(a, b)
|
||||
return List.new(vim.list_slice(self, a, b))
|
||||
end
|
||||
|
||||
--- Similar to slice, but with every element. It only makes a shallow copy
|
||||
--- @return List: A slice from 1 to #self, i.e., a complete copy of the list
|
||||
--- @see List.deep_copy
|
||||
function List:copy()
|
||||
return self:slice(1, #self)
|
||||
end
|
||||
|
||||
--- Similar to copy, but makes a deep copy instead
|
||||
--- @return List: A deep copy of the object
|
||||
--- @see List.copy
|
||||
--- @see vim.deep_copy
|
||||
function List:deep_copy()
|
||||
return vim.deep_copy(self)
|
||||
end
|
||||
|
||||
--- Reverses the list in place. If you don't want this, you could do something
|
||||
--- like this
|
||||
--- <pre>
|
||||
--- local list = List{1, 2, 3, 4}
|
||||
--- local reversed = list:copy():reverse()
|
||||
--- </pre>
|
||||
--- @return List: The list itself, so you can chain method calls
|
||||
--- @see List.copy
|
||||
--- @see List.deep_copy
|
||||
function List:reverse()
|
||||
local n = #self
|
||||
local i = 1
|
||||
while i < n do
|
||||
self[i], self[n] = self[n], self[i]
|
||||
i = i + 1
|
||||
n = n - 1
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Concatenates the elements whithin the list separated by the given string
|
||||
--- <pre>
|
||||
--- local list = List{1, 2, 3, 4}
|
||||
--- print(list:join('-')) -- 1-2-3-4
|
||||
--- </pre>
|
||||
--- @param sep string: The separator to place between the elements. Default ''
|
||||
--- @return string: The elements in the list separated by sep
|
||||
function List:join(sep)
|
||||
sep = sep or ""
|
||||
local result = ""
|
||||
for i, v in self:iter() do
|
||||
result = result .. tostring(v)
|
||||
if i ~= #self then
|
||||
result = result .. sep
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
--- Returns a list with the elements of self concatenated with those in the
|
||||
--- given arguments
|
||||
--- @vararg table|List: The sequences to concatenate to this one
|
||||
--- @return List
|
||||
function List:concat(...)
|
||||
local result = self:copy()
|
||||
local others = { ... }
|
||||
for _, other in ipairs(others) do
|
||||
for _, v in ipairs(other) do
|
||||
result:push(v)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
--- Moves the elements between from and from+len in self, to positions between
|
||||
--- to and to+len in other, like so
|
||||
--- <pre>
|
||||
--- other[to], other[to+1]... other[to+len] = self[from], self[from+1]... self[from+len]
|
||||
--- </pre>
|
||||
--- @param from number: The first index of the origin slice
|
||||
--- @param len number: The length of the slices
|
||||
--- @param to number: The first index of the destination slice
|
||||
--- @param other table|List: The destination list. Defaults to self
|
||||
--- @see table.move
|
||||
function List:move(from, len, to, other)
|
||||
return table.move(self, from, len, to, other)
|
||||
end
|
||||
|
||||
--- Packs the given elements into a list. Similar to lua 5.3's table.pack
|
||||
--- @vararg any: The elements to pack
|
||||
--- @return List: a list containing all the given elements
|
||||
--- @see table.pack
|
||||
function List.pack(...)
|
||||
return List.new { ... }
|
||||
end
|
||||
|
||||
--- Unpacks the elements from this list and returns them
|
||||
--- @return ...any: All the elements from self[1] to self[#self]
|
||||
function List:unpack()
|
||||
return unpack(self, 1, #self)
|
||||
end
|
||||
|
||||
-- Iterator stuff
|
||||
|
||||
local Iter = require "plenary.iterators"
|
||||
|
||||
local itermetatable = getmetatable(Iter:wrap())
|
||||
|
||||
local function forward_list_gen(param, state)
|
||||
state = state + 1
|
||||
local v = param[state]
|
||||
if v ~= nil then
|
||||
return state, v
|
||||
end
|
||||
end
|
||||
|
||||
local function backward_list_gen(param, state)
|
||||
state = state - 1
|
||||
local v = param[state]
|
||||
if v ~= nil then
|
||||
return state, v
|
||||
end
|
||||
end
|
||||
|
||||
--- Run the given predicate through all the elements pointed by this iterator,
|
||||
--- and classify them into two lists. The first one holds the elements for which
|
||||
--- predicate returned a truthy value, and the second holds the rest. For
|
||||
--- example:
|
||||
---
|
||||
--- <pre>
|
||||
--- local list = List{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
--- local evens, odds = list:iter():partition(function(e)
|
||||
--- return e % 2 == 0
|
||||
--- end)
|
||||
--- print(evens, odds)
|
||||
--- </pre>
|
||||
---
|
||||
--- Would print
|
||||
---
|
||||
--- <pre>
|
||||
--- [0, 2, 4, 6, 8] [1, 3, 5, 7, 9]
|
||||
--- </pre>
|
||||
---@param predicate function: The predicate to classify the elements
|
||||
---@return List,List
|
||||
local function partition(self, predicate)
|
||||
local list1, list2 = List.new {}, List.new {}
|
||||
for _, v in self do
|
||||
if predicate(v) then
|
||||
list1:push(v)
|
||||
else
|
||||
list2:push(v)
|
||||
end
|
||||
end
|
||||
return list1, list2
|
||||
end
|
||||
|
||||
local function wrap_iter(f, l, n)
|
||||
local iter = Iter.wrap(f, l, n)
|
||||
iter.partition = partition
|
||||
return iter
|
||||
end
|
||||
|
||||
--- Counts the occurrences of e inside the list
|
||||
--- @param e any: The element to test for
|
||||
--- @return number: The number of occurrences of e
|
||||
function List:count(e)
|
||||
local count = 0
|
||||
for _, v in self:iter() do
|
||||
if e == v then
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
--- Appends the elements in the given iterator to the list
|
||||
--- @param other table: An iterator object
|
||||
function List:extend(other)
|
||||
if type(other) == "table" and getmetatable(other) == itermetatable then
|
||||
for _, v in other do
|
||||
self:push(v)
|
||||
end
|
||||
else
|
||||
error "Argument must be an iterator"
|
||||
end
|
||||
end
|
||||
|
||||
--- Checks whether there is an occurence of the given element in the list
|
||||
--- @param e any: The object to test for
|
||||
--- @return boolean: True if e is present
|
||||
function List:contains(e)
|
||||
for _, v in self:iter() do
|
||||
if v == e then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- Creates an iterator for the list. For example:
|
||||
--- <pre>
|
||||
--- local list = List{8, 4, 7, 9}
|
||||
--- for i, v in list:iter() do
|
||||
--- print(i, v)
|
||||
--- end
|
||||
--- </pre>
|
||||
--- Would print:
|
||||
--- <pre>
|
||||
--- 1 8
|
||||
--- 2 4
|
||||
--- 3 7
|
||||
--- 4 9
|
||||
--- </pre>
|
||||
--- @return table: An iterator object
|
||||
function List:iter()
|
||||
return wrap_iter(forward_list_gen, self, 0)
|
||||
end
|
||||
|
||||
--- Creates a reverse iterator for the list. For example:
|
||||
--- <pre>
|
||||
--- local list = List{8, 4, 7, 9}
|
||||
--- for i, v in list:riter() do
|
||||
--- print(i, v)
|
||||
--- end
|
||||
--- </pre>
|
||||
--- Would print:
|
||||
--- <pre>
|
||||
--- 4 9
|
||||
--- 3 7
|
||||
--- 2 4
|
||||
--- 1 8
|
||||
--- </pre>
|
||||
--- @return table: An iterator object
|
||||
function List:riter()
|
||||
return wrap_iter(backward_list_gen, self, #self + 1)
|
||||
end
|
||||
|
||||
-- Miscellaneous
|
||||
|
||||
--- Create a list from the elements pointed at by the given iterator.
|
||||
--- @param iter table: An iterator object
|
||||
--- @return List
|
||||
function List.from_iter(iter)
|
||||
local result = List.new {}
|
||||
for _, v in iter do
|
||||
result:push(v)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
return setmetatable({}, {
|
||||
__call = function(_, tbl)
|
||||
return List.new(tbl)
|
||||
end,
|
||||
__index = List,
|
||||
})
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
local m = {}
|
||||
|
||||
m.flatten = (function()
|
||||
if vim.fn.has "nvim-0.11" == 1 then
|
||||
return function(t)
|
||||
return vim.iter(t):flatten():totable()
|
||||
end
|
||||
else
|
||||
return function(t)
|
||||
return vim.tbl_flatten(t)
|
||||
end
|
||||
end
|
||||
end)()
|
||||
|
||||
m.islist = vim.islist or vim.tbl_islist
|
||||
|
||||
return m
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
--- I like context managers for Python
|
||||
--- I want them in Lua.
|
||||
|
||||
local context_manager = {}
|
||||
|
||||
function context_manager.with(obj, callable)
|
||||
-- Wrap functions for people since we're nice
|
||||
if type(obj) == "function" then
|
||||
obj = coroutine.create(obj)
|
||||
end
|
||||
|
||||
if type(obj) == "thread" then
|
||||
local ok, context = coroutine.resume(obj)
|
||||
assert(ok, "Should have yielded in coroutine.")
|
||||
|
||||
local succeeded, result = pcall(callable, context)
|
||||
|
||||
local done, _ = coroutine.resume(obj)
|
||||
assert(done, "Should be done")
|
||||
|
||||
local no_other = not coroutine.resume(obj)
|
||||
assert(no_other, "Should not yield anymore, otherwise that would make things complicated")
|
||||
|
||||
assert(succeeded, result)
|
||||
return result
|
||||
else
|
||||
assert(obj.enter)
|
||||
assert(obj.exit)
|
||||
|
||||
-- TODO: Callable can be string for vimL function or a lua callable
|
||||
local context = obj:enter()
|
||||
local succeeded, result = pcall(callable, context)
|
||||
obj:exit()
|
||||
|
||||
assert(succeeded, result)
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
--- @param filename string|table -- If string, used as io.open(filename)
|
||||
--- Else, should be a table with `filename` as an attribute
|
||||
function context_manager.open(filename, mode)
|
||||
if type(filename) == "table" and filename.filename then
|
||||
filename = filename.filename
|
||||
end
|
||||
|
||||
local file_io = assert(io.open(filename, mode))
|
||||
|
||||
return coroutine.create(function()
|
||||
coroutine.yield(file_io)
|
||||
|
||||
file_io:close()
|
||||
end)
|
||||
end
|
||||
|
||||
return context_manager
|
||||
359
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/curl.lua
Normal file
359
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/curl.lua
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
--[[
|
||||
Curl Wrapper
|
||||
|
||||
all curl methods accepts
|
||||
|
||||
url = "The url to make the request to.", (string)
|
||||
query = "url query, append after the url", (table)
|
||||
body = "The request body" (string/filepath/table)
|
||||
auth = "Basic request auth, 'user:pass', or {"user", "pass"}" (string/array)
|
||||
form = "request form" (table)
|
||||
raw = "any additonal curl args, it must be an array/list." (array)
|
||||
dry_run = "whether to return the args to be ran through curl." (boolean)
|
||||
output = "where to download something." (filepath)
|
||||
timeout = "request timeout in mseconds" (number)
|
||||
http_version = "HTTP version to use: 'HTTP/0.9', 'HTTP/1.0', 'HTTP/1.1', 'HTTP/2', or 'HTTP/3'" (string)
|
||||
proxy = "[protocol://]host[:port] Use this proxy" (string)
|
||||
insecure = "Allow insecure server connections" (boolean)
|
||||
|
||||
and returns table:
|
||||
|
||||
exit = "The shell process exit code." (number)
|
||||
status = "The https response status." (number)
|
||||
headers = "The https response headers." (array)
|
||||
body = "The http response body." (string)
|
||||
|
||||
see test/plenary/curl_spec.lua for examples.
|
||||
|
||||
author = github.com/tami5
|
||||
]]
|
||||
--
|
||||
|
||||
local util, parse = {}, {}
|
||||
|
||||
-- Helpers --------------------------------------------------
|
||||
-------------------------------------------------------------
|
||||
local F = require "plenary.functional"
|
||||
local J = require "plenary.job"
|
||||
local P = require "plenary.path"
|
||||
local compat = require "plenary.compat"
|
||||
|
||||
-- Utils ----------------------------------------------------
|
||||
-------------------------------------------------------------
|
||||
|
||||
util.url_encode = function(str)
|
||||
if type(str) ~= "number" then
|
||||
str = str:gsub("\r?\n", "\r\n")
|
||||
str = str:gsub("([^%w%-%.%_%~ ])", function(c)
|
||||
return string.format("%%%02X", c:byte())
|
||||
end)
|
||||
str = str:gsub(" ", "+")
|
||||
return str
|
||||
else
|
||||
return str
|
||||
end
|
||||
end
|
||||
|
||||
util.kv_to_list = function(kv, prefix, sep)
|
||||
return compat.flatten(F.kv_map(function(kvp)
|
||||
return { prefix, kvp[1] .. sep .. kvp[2] }
|
||||
end, kv))
|
||||
end
|
||||
|
||||
util.kv_to_str = function(kv, sep, kvsep)
|
||||
return F.join(
|
||||
F.kv_map(function(kvp)
|
||||
return kvp[1] .. kvsep .. util.url_encode(kvp[2])
|
||||
end, kv),
|
||||
sep
|
||||
)
|
||||
end
|
||||
|
||||
util.gen_dump_path = function()
|
||||
local path
|
||||
local id = string.gsub("xxxx4xxx", "[xy]", function(l)
|
||||
local v = (l == "x") and math.random(0, 0xf) or math.random(0, 0xb)
|
||||
return string.format("%x", v)
|
||||
end)
|
||||
if P.path.sep == "\\" then
|
||||
path = string.format("%s\\AppData\\Local\\Temp\\plenary_curl_%s.headers", os.getenv "USERPROFILE", id)
|
||||
else
|
||||
local temp_dir = os.getenv "XDG_RUNTIME_DIR" or "/tmp"
|
||||
path = temp_dir .. "/plenary_curl_" .. id .. ".headers"
|
||||
end
|
||||
return { "-D", path }
|
||||
end
|
||||
|
||||
-- Parsers ----------------------------------------------------
|
||||
---------------------------------------------------------------
|
||||
|
||||
parse.headers = function(t)
|
||||
if not t then
|
||||
return
|
||||
end
|
||||
local upper = function(str)
|
||||
return string.gsub(" " .. str, "%W%l", string.upper):sub(2)
|
||||
end
|
||||
return util.kv_to_list(
|
||||
(function()
|
||||
local normilzed = {}
|
||||
for k, v in pairs(t) do
|
||||
normilzed[upper(k:gsub("_", "%-"))] = v
|
||||
end
|
||||
return normilzed
|
||||
end)(),
|
||||
"-H",
|
||||
": "
|
||||
)
|
||||
end
|
||||
|
||||
parse.data_body = function(t)
|
||||
if not t then
|
||||
return
|
||||
end
|
||||
return util.kv_to_list(t, "-d", "=")
|
||||
end
|
||||
|
||||
parse.raw_body = function(xs)
|
||||
if not xs then
|
||||
return
|
||||
end
|
||||
if type(xs) == "table" then
|
||||
return parse.data_body(xs)
|
||||
else
|
||||
return { "--data-raw", xs }
|
||||
end
|
||||
end
|
||||
|
||||
parse.form = function(t)
|
||||
if not t then
|
||||
return
|
||||
end
|
||||
return util.kv_to_list(t, "-F", "=")
|
||||
end
|
||||
|
||||
parse.curl_query = function(t)
|
||||
if not t then
|
||||
return
|
||||
end
|
||||
return util.kv_to_str(t, "&", "=")
|
||||
end
|
||||
|
||||
parse.method = function(s)
|
||||
if not s then
|
||||
return
|
||||
end
|
||||
if s ~= "head" then
|
||||
return { "-X", string.upper(s) }
|
||||
else
|
||||
return { "-I" }
|
||||
end
|
||||
end
|
||||
|
||||
parse.file = function(p)
|
||||
if not p then
|
||||
return
|
||||
end
|
||||
return { "-d", "@" .. P.expand(P.new(p)) }
|
||||
end
|
||||
|
||||
parse.auth = function(xs)
|
||||
if not xs then
|
||||
return
|
||||
end
|
||||
return { "-u", type(xs) == "table" and util.kv_to_str(xs, nil, ":") or xs }
|
||||
end
|
||||
|
||||
parse.url = function(xs, q)
|
||||
if not xs then
|
||||
return
|
||||
end
|
||||
q = parse.curl_query(q)
|
||||
if type(xs) == "string" then
|
||||
return q and xs .. "?" .. q or xs
|
||||
elseif type(xs) == "table" then
|
||||
error "Low level URL definition is not supported."
|
||||
end
|
||||
end
|
||||
|
||||
parse.accept_header = function(s)
|
||||
if not s then
|
||||
return
|
||||
end
|
||||
return { "-H", "Accept: " .. s }
|
||||
end
|
||||
|
||||
parse.http_version = function(s)
|
||||
if not s then
|
||||
return
|
||||
end
|
||||
if s == "HTTP/0.9" or s == "HTTP/1.0" or s == "HTTP/1.1" or s == "HTTP/2" or s == "HTTP/3" then
|
||||
s = s:lower()
|
||||
s = s:gsub("/", "")
|
||||
return { "--" .. s }
|
||||
else
|
||||
error "Unknown HTTP version."
|
||||
end
|
||||
end
|
||||
|
||||
-- Parse Request -------------------------------------------
|
||||
------------------------------------------------------------
|
||||
parse.request = function(opts)
|
||||
if opts.body then
|
||||
local b = opts.body
|
||||
local silent_is_file = function()
|
||||
local status, result = pcall(P.is_file, P.new(b))
|
||||
return status and result
|
||||
end
|
||||
opts.body = nil
|
||||
if type(b) == "table" then
|
||||
opts.data = b
|
||||
elseif silent_is_file() then
|
||||
opts.in_file = b
|
||||
elseif type(b) == "string" then
|
||||
opts.raw_body = b
|
||||
end
|
||||
end
|
||||
local result = { "-sSL", opts.dump }
|
||||
local append = function(v)
|
||||
if v then
|
||||
table.insert(result, v)
|
||||
end
|
||||
end
|
||||
|
||||
if opts.insecure then
|
||||
table.insert(result, "--insecure")
|
||||
end
|
||||
if opts.proxy then
|
||||
table.insert(result, { "--proxy", opts.proxy })
|
||||
end
|
||||
if opts.compressed then
|
||||
table.insert(result, "--compressed")
|
||||
end
|
||||
append(parse.method(opts.method))
|
||||
append(parse.headers(opts.headers))
|
||||
append(parse.accept_header(opts.accept))
|
||||
append(parse.raw_body(opts.raw_body))
|
||||
append(parse.data_body(opts.data))
|
||||
append(parse.form(opts.form))
|
||||
append(parse.file(opts.in_file))
|
||||
append(parse.auth(opts.auth))
|
||||
append(parse.http_version(opts.http_version))
|
||||
append(opts.raw)
|
||||
if opts.output then
|
||||
table.insert(result, { "-o", opts.output })
|
||||
end
|
||||
table.insert(result, parse.url(opts.url, opts.query))
|
||||
return compat.flatten(result), opts
|
||||
end
|
||||
|
||||
-- Parse response ------------------------------------------
|
||||
------------------------------------------------------------
|
||||
parse.response = function(lines, dump_path, code)
|
||||
local headers = P.readlines(dump_path)
|
||||
local status = nil
|
||||
local processed_headers = {}
|
||||
|
||||
-- Process headers in a single pass
|
||||
for _, line in ipairs(headers) do
|
||||
local status_match = line:match "^HTTP/%S*%s+(%d+)"
|
||||
if status_match then
|
||||
status = tonumber(status_match)
|
||||
elseif line ~= "" then
|
||||
table.insert(processed_headers, line)
|
||||
end
|
||||
end
|
||||
|
||||
local body = F.join(lines, "\n")
|
||||
vim.loop.fs_unlink(dump_path)
|
||||
|
||||
return {
|
||||
status = status or 0,
|
||||
headers = processed_headers,
|
||||
body = body,
|
||||
exit = code,
|
||||
}
|
||||
end
|
||||
|
||||
local request = function(specs)
|
||||
local response = {}
|
||||
local args, opts = parse.request(vim.tbl_extend("force", {
|
||||
compressed = package.config:sub(1, 1) ~= "\\",
|
||||
dry_run = false,
|
||||
dump = util.gen_dump_path(),
|
||||
}, specs))
|
||||
|
||||
if opts.dry_run then
|
||||
return args
|
||||
end
|
||||
|
||||
local job_opts = {
|
||||
command = vim.g.plenary_curl_bin_path or "curl",
|
||||
args = args,
|
||||
}
|
||||
|
||||
if opts.stream then
|
||||
job_opts.on_stdout = opts.stream
|
||||
end
|
||||
|
||||
job_opts.on_exit = function(j, code)
|
||||
if code ~= 0 then
|
||||
local stderr = vim.inspect(j:stderr_result())
|
||||
local message = string.format("%s %s - curl error exit_code=%s stderr=%s", opts.method, opts.url, code, stderr)
|
||||
if opts.on_error then
|
||||
return opts.on_error {
|
||||
message = message,
|
||||
stderr = stderr,
|
||||
exit = code,
|
||||
}
|
||||
else
|
||||
error(message)
|
||||
end
|
||||
end
|
||||
local output = parse.response(j:result(), opts.dump[2], code)
|
||||
if opts.callback then
|
||||
return opts.callback(output)
|
||||
else
|
||||
response = output
|
||||
end
|
||||
end
|
||||
local job = J:new(job_opts)
|
||||
|
||||
if opts.callback or opts.stream then
|
||||
job:start()
|
||||
return job
|
||||
else
|
||||
local timeout = opts.timeout or 10000
|
||||
job:sync(timeout)
|
||||
return response
|
||||
end
|
||||
end
|
||||
|
||||
-- Main ----------------------------------------------------
|
||||
------------------------------------------------------------
|
||||
return (function()
|
||||
local partial = function(method)
|
||||
return function(url, opts)
|
||||
local spec = {}
|
||||
opts = opts or {}
|
||||
if type(url) == "table" then
|
||||
opts = url
|
||||
spec.method = method
|
||||
else
|
||||
spec.url = url
|
||||
spec.method = method
|
||||
end
|
||||
opts = method == "request" and opts or (vim.tbl_extend("keep", opts, spec))
|
||||
return request(opts)
|
||||
end
|
||||
end
|
||||
return {
|
||||
get = partial "get",
|
||||
post = partial "post",
|
||||
put = partial "put",
|
||||
head = partial "head",
|
||||
patch = partial "patch",
|
||||
delete = partial "delete",
|
||||
request = partial "request",
|
||||
}
|
||||
end)()
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
local debug_utils = {}
|
||||
|
||||
function debug_utils.sourced_filepath()
|
||||
local str = debug.getinfo(2, "S").source:sub(2)
|
||||
return str
|
||||
end
|
||||
|
||||
function debug_utils.sourced_filename()
|
||||
local str = debug_utils.sourced_filepath()
|
||||
return str:match "^.*/(.*).lua$" or str
|
||||
end
|
||||
|
||||
return debug_utils
|
||||
162
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/enum.lua
Normal file
162
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/enum.lua
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
---@brief [[
|
||||
--- This module defines an idiomatic way to create enum classes, similar to
|
||||
--- those in java or kotlin. There are two ways to create an enum, one is with
|
||||
--- the exported `make_enum` function, or calling the module directly with the
|
||||
--- enum spec.
|
||||
---
|
||||
--- The enum spec consists of a list-like table whose members can be either a
|
||||
--- string or a tuple of the form {string, number}. In the former case, the enum
|
||||
--- member will take the next available value, while in the latter, the member
|
||||
--- will take the string as it's name and the number as it's value. In both
|
||||
--- cases, the name must start with a capital letter.
|
||||
---
|
||||
--- Here is an example:
|
||||
---
|
||||
--- <pre>
|
||||
--- local Enum = require 'plenary.enum'
|
||||
--- local myEnum = Enum {
|
||||
--- 'Foo', -- Takes value 1
|
||||
--- 'Bar', -- Takes value 2
|
||||
--- {'Qux', 10}, -- Takes value 10
|
||||
--- 'Baz', -- Takes value 11
|
||||
--- }
|
||||
--- </pre>
|
||||
---
|
||||
--- In case of name or value clashing, the call will fail. For this reason, it's
|
||||
--- best if you define the members in ascending order.
|
||||
---@brief ]]
|
||||
local Enum = {}
|
||||
|
||||
---@class Enum
|
||||
|
||||
---@class Variant
|
||||
|
||||
local function validate_member_name(name)
|
||||
if #name > 0 and name:sub(1, 1):match "%u" then
|
||||
return name
|
||||
end
|
||||
error('"' .. name .. '" should start with a capital letter')
|
||||
end
|
||||
|
||||
--- Creates an enum from the given list-like table, like so:
|
||||
--- <pre>
|
||||
--- local enum = Enum.make_enum{
|
||||
--- 'Foo',
|
||||
--- 'Bar',
|
||||
--- {'Qux', 10}
|
||||
--- }
|
||||
--- </pre>
|
||||
--- @return Enum: A new enum
|
||||
local function make_enum(tbl)
|
||||
local enum = {}
|
||||
|
||||
local Variant = {}
|
||||
Variant.__index = Variant
|
||||
|
||||
local function newVariant(i)
|
||||
return setmetatable({ value = i }, Variant)
|
||||
end
|
||||
|
||||
-- we don't need __eq because the __eq metamethod will only ever be
|
||||
-- invoked when they both have the same metatable
|
||||
|
||||
function Variant:__lt(o)
|
||||
return self.value < o.value
|
||||
end
|
||||
|
||||
function Variant:__gt(o)
|
||||
return self.value > o.value
|
||||
end
|
||||
|
||||
function Variant:__tostring()
|
||||
return tostring(self.value)
|
||||
end
|
||||
|
||||
local function find_next_idx(e, i)
|
||||
local newI = i + 1
|
||||
if not e[newI] then
|
||||
return newI
|
||||
end
|
||||
error("Overlapping index: " .. tostring(newI))
|
||||
end
|
||||
|
||||
local i = 0
|
||||
|
||||
for _, v in ipairs(tbl) do
|
||||
if type(v) == "string" then
|
||||
local name = validate_member_name(v)
|
||||
local idx = find_next_idx(enum, i)
|
||||
enum[idx] = name
|
||||
if enum[name] then
|
||||
error("Duplicate enum member name: " .. name)
|
||||
end
|
||||
enum[name] = newVariant(idx)
|
||||
i = idx
|
||||
elseif type(v) == "table" and type(v[1]) == "string" and type(v[2]) == "number" then
|
||||
local name = validate_member_name(v[1])
|
||||
local idx = v[2]
|
||||
if enum[idx] then
|
||||
error("Overlapping index: " .. tostring(idx))
|
||||
end
|
||||
enum[idx] = name
|
||||
if enum[name] then
|
||||
error("Duplicate name: " .. name)
|
||||
end
|
||||
enum[name] = newVariant(idx)
|
||||
i = idx
|
||||
else
|
||||
error "Invalid way to specify an enum variant"
|
||||
end
|
||||
end
|
||||
|
||||
return require("plenary.tbl").freeze(setmetatable(enum, Enum))
|
||||
end
|
||||
|
||||
Enum.__index = function(_, key)
|
||||
if Enum[key] then
|
||||
return Enum[key]
|
||||
end
|
||||
error("Invalid enum key: " .. tostring(key))
|
||||
end
|
||||
|
||||
--- Checks whether the enum has a member with the given name
|
||||
--- @param key string: The element to check for
|
||||
--- @return boolean: True if key is present
|
||||
function Enum:has_key(key)
|
||||
if rawget(getmetatable(self).__index, key) then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- If there is a member named 'key', return it, otherwise return nil
|
||||
--- @param key string: The element to check for
|
||||
--- @return Variant: The element named by key, or nil if not present
|
||||
function Enum:from_str(key)
|
||||
if self:has_key(key) then
|
||||
return self[key]
|
||||
end
|
||||
end
|
||||
|
||||
--- If there is a member of value 'num', return it, otherwise return nil
|
||||
--- @param num number: The value of the element to check for
|
||||
--- @return Variant: The element whose value is num
|
||||
function Enum:from_num(num)
|
||||
local key = self[num]
|
||||
if key then
|
||||
return self[key]
|
||||
end
|
||||
end
|
||||
|
||||
--- Checks whether the given object corresponds to an instance of Enum
|
||||
--- @param tbl table: The object to be checked
|
||||
--- @return boolean: True if tbl is an Enum
|
||||
local function is_enum(tbl)
|
||||
return getmetatable(getmetatable(tbl).__index) == Enum
|
||||
end
|
||||
|
||||
return setmetatable({ is_enum = is_enum, make_enum = make_enum }, {
|
||||
__call = function(_, tbl)
|
||||
return make_enum(tbl)
|
||||
end,
|
||||
})
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
local M = {}
|
||||
|
||||
M.traceback_error = function(s, level)
|
||||
local traceback = debug.traceback()
|
||||
traceback = traceback .. "\n" .. s
|
||||
error(traceback, (level or 1) + 1)
|
||||
end
|
||||
|
||||
M.info_error = function(s, func_info, level)
|
||||
local info = debug.getinfo(func_info)
|
||||
info = info .. "\n" .. s
|
||||
error(info, (level or 1) + 1)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
local Path = require "plenary.path"
|
||||
|
||||
local os_sep = Path.path.sep
|
||||
|
||||
local filetype = {}
|
||||
|
||||
local filetype_table = {
|
||||
extension = {},
|
||||
file_name = {},
|
||||
shebang = {},
|
||||
}
|
||||
|
||||
filetype.add_table = function(new_filetypes)
|
||||
local valid_keys = { "extension", "file_name", "shebang" }
|
||||
local new_keys = {}
|
||||
|
||||
-- Validate keys
|
||||
for k, _ in pairs(new_filetypes) do
|
||||
new_keys[k] = true
|
||||
end
|
||||
for _, k in ipairs(valid_keys) do
|
||||
new_keys[k] = nil
|
||||
end
|
||||
|
||||
for k, v in pairs(new_keys) do
|
||||
error(debug.traceback("Invalid key / value:" .. tostring(k) .. " / " .. tostring(v)))
|
||||
end
|
||||
|
||||
if new_filetypes.extension then
|
||||
filetype_table.extension = vim.tbl_extend("force", filetype_table.extension, new_filetypes.extension)
|
||||
end
|
||||
|
||||
if new_filetypes.file_name then
|
||||
filetype_table.file_name = vim.tbl_extend("force", filetype_table.file_name, new_filetypes.file_name)
|
||||
end
|
||||
|
||||
if new_filetypes.shebang then
|
||||
filetype_table.shebang = vim.tbl_extend("force", filetype_table.shebang, new_filetypes.shebang)
|
||||
end
|
||||
end
|
||||
|
||||
filetype.add_file = function(filename)
|
||||
local filetype_files = vim.api.nvim_get_runtime_file(string.format("data/plenary/filetypes/%s.lua", filename), true)
|
||||
|
||||
for _, file in ipairs(filetype_files) do
|
||||
local ok, msg = pcall(filetype.add_table, dofile(file))
|
||||
if not ok then
|
||||
error("Unable to add file " .. file .. ":\n" .. msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local filename_regex = "[^" .. os_sep .. "].*"
|
||||
filetype._get_extension_parts = function(filename)
|
||||
local current_match = filename:match(filename_regex)
|
||||
local possibilities = {}
|
||||
while current_match do
|
||||
current_match = current_match:match "[^.]%.(.*)"
|
||||
if current_match then
|
||||
table.insert(possibilities, current_match:lower())
|
||||
else
|
||||
return possibilities
|
||||
end
|
||||
end
|
||||
return possibilities
|
||||
end
|
||||
|
||||
filetype._parse_modeline = function(tail)
|
||||
if tail:find "vim:" then
|
||||
return tail:match ".*:ft=([^: ]*):.*$" or ""
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
filetype._parse_shebang = function(head)
|
||||
if head:sub(1, 2) == "#!" then
|
||||
local match = filetype_table.shebang[head:sub(3, #head)]
|
||||
if match then
|
||||
return match
|
||||
end
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
local done_adding = false
|
||||
local extend_tbl_with_ext_eq_ft_entries = function()
|
||||
if not done_adding then
|
||||
if vim.in_fast_event() then
|
||||
return
|
||||
end
|
||||
local all_valid_filetypes = vim.fn.getcompletion("", "filetype")
|
||||
for _, v in ipairs(all_valid_filetypes) do
|
||||
if not filetype_table.extension[v] then
|
||||
filetype_table.extension[v] = v
|
||||
end
|
||||
end
|
||||
done_adding = true
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
filetype.detect_from_extension = function(filepath)
|
||||
local exts = filetype._get_extension_parts(filepath)
|
||||
for _, ext in ipairs(exts) do
|
||||
local match = ext and filetype_table.extension[ext]
|
||||
if match then
|
||||
return match
|
||||
end
|
||||
end
|
||||
if extend_tbl_with_ext_eq_ft_entries() then
|
||||
for _, ext in ipairs(exts) do
|
||||
local match = ext and filetype_table.extension[ext]
|
||||
if match then
|
||||
return match
|
||||
end
|
||||
end
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
filetype.detect_from_name = function(filepath)
|
||||
if filepath then
|
||||
filepath = filepath:lower()
|
||||
local split_path = vim.split(filepath, os_sep, true)
|
||||
local fname = split_path[#split_path]
|
||||
local match = filetype_table.file_name[fname]
|
||||
if match then
|
||||
return match
|
||||
end
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
filetype.detect_from_modeline = function(filepath)
|
||||
local tail = Path:new(filepath):readbyterange(-256, 256)
|
||||
if not tail then
|
||||
return ""
|
||||
end
|
||||
local lines = vim.split(tail, "\n")
|
||||
local idx = lines[#lines] ~= "" and #lines or #lines - 1
|
||||
if idx >= 1 then
|
||||
return filetype._parse_modeline(lines[idx])
|
||||
end
|
||||
end
|
||||
|
||||
filetype.detect_from_shebang = function(filepath)
|
||||
local head = Path:new(filepath):readbyterange(0, 256)
|
||||
if not head then
|
||||
return ""
|
||||
end
|
||||
local lines = vim.split(head, "\n")
|
||||
return filetype._parse_shebang(lines[1])
|
||||
end
|
||||
|
||||
--- Detect a filetype from a path.
|
||||
---
|
||||
---@param opts table: Table with optional keys
|
||||
--- - fs_access (bool, default=true): Should check a file if it exists
|
||||
filetype.detect = function(filepath, opts)
|
||||
opts = opts or {}
|
||||
opts.fs_access = opts.fs_access or true
|
||||
|
||||
if type(filepath) ~= string then
|
||||
filepath = tostring(filepath)
|
||||
end
|
||||
|
||||
local match = filetype.detect_from_name(filepath)
|
||||
if match ~= "" then
|
||||
return match
|
||||
end
|
||||
|
||||
match = filetype.detect_from_extension(filepath)
|
||||
|
||||
if opts.fs_access and Path:new(filepath):exists() then
|
||||
if match == "" then
|
||||
match = filetype.detect_from_shebang(filepath)
|
||||
if match ~= "" then
|
||||
return match
|
||||
end
|
||||
end
|
||||
|
||||
if match == "text" or match == "" then
|
||||
match = filetype.detect_from_modeline(filepath)
|
||||
if match ~= "" then
|
||||
return match
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return match
|
||||
end
|
||||
|
||||
filetype.add_file "base"
|
||||
filetype.add_file "builtin"
|
||||
|
||||
return filetype
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
local M = {}
|
||||
|
||||
M.bind = require("plenary.functional").partial
|
||||
|
||||
function M.arify(fn, argc)
|
||||
return function(...)
|
||||
if select("#", ...) ~= argc then
|
||||
error(("Expected %s number of arguments"):format(argc))
|
||||
end
|
||||
|
||||
fn(...)
|
||||
end
|
||||
end
|
||||
|
||||
function M.create_wrapper(map)
|
||||
return function(to_wrap)
|
||||
return function(...)
|
||||
return map(to_wrap(...))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
local f = {}
|
||||
|
||||
function f.kv_pairs(t)
|
||||
local results = {}
|
||||
for k, v in pairs(t) do
|
||||
table.insert(results, { k, v })
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
function f.kv_map(fun, t)
|
||||
return vim.tbl_map(fun, f.kv_pairs(t))
|
||||
end
|
||||
|
||||
function f.join(array, sep)
|
||||
return table.concat(vim.tbl_map(tostring, array), sep)
|
||||
end
|
||||
|
||||
local function bind_n(fn, n, a, ...)
|
||||
if n == 0 then
|
||||
return fn
|
||||
end
|
||||
return bind_n(function(...)
|
||||
return fn(a, ...)
|
||||
end, n - 1, ...)
|
||||
end
|
||||
|
||||
function f.partial(fun, ...)
|
||||
return bind_n(fun, select("#", ...), ...)
|
||||
end
|
||||
|
||||
function f.any(fun, iterable)
|
||||
for k, v in pairs(iterable) do
|
||||
if fun(k, v) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function f.all(fun, iterable)
|
||||
for k, v in pairs(iterable) do
|
||||
if not fun(k, v) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function f.if_nil(val, was_nil, was_not_nil)
|
||||
if val == nil then
|
||||
return was_nil
|
||||
else
|
||||
return was_not_nil
|
||||
end
|
||||
end
|
||||
|
||||
function f.select_only(n)
|
||||
return function(...)
|
||||
local x = select(n, ...)
|
||||
return x
|
||||
end
|
||||
end
|
||||
|
||||
f.first = f.select_only(1)
|
||||
f.second = f.select_only(2)
|
||||
f.third = f.select_only(3)
|
||||
|
||||
function f.last(...)
|
||||
local length = select("#", ...)
|
||||
local x = select(length, ...)
|
||||
return x
|
||||
end
|
||||
|
||||
return f
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
-- Lazy load everything into plenary.
|
||||
local plenary = setmetatable({}, {
|
||||
__index = function(t, k)
|
||||
local ok, val = pcall(require, string.format("plenary.%s", k))
|
||||
|
||||
if ok then
|
||||
rawset(t, k, val)
|
||||
end
|
||||
|
||||
return val
|
||||
end,
|
||||
})
|
||||
|
||||
return plenary
|
||||
|
|
@ -0,0 +1,695 @@
|
|||
---@brief [[
|
||||
---An adaptation of luafun for neovim.
|
||||
---This library will use neovim specific functions.
|
||||
---Some documentation is the same as from luafun.
|
||||
---Some extra functions are present that are not in luafun
|
||||
---@brief ]]
|
||||
|
||||
local co = coroutine
|
||||
local f = require "plenary.functional"
|
||||
local compat = require "plenary.compat"
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Tools
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local exports = {}
|
||||
|
||||
---@class Iterator
|
||||
---@field gen function
|
||||
---@field param any
|
||||
---@field state any
|
||||
local Iterator = {}
|
||||
Iterator.__index = Iterator
|
||||
|
||||
---Makes a for loop work
|
||||
---If not called without param or state, will just generate with the starting state
|
||||
---This is useful because the original luafun will also return param and state in addition to the iterator as a multival
|
||||
---This can cause problems because when using iterators as expressions the multivals can bleed
|
||||
---For example i.iter { 1, 2, i.iter { 3, 4 } } will not work because the inner iterator returns a multival thus
|
||||
---polluting the list with internal values.
|
||||
---So instead we do not return param and state as multivals when doing wrap
|
||||
---This causes the first loop iteration to call param and state with nil because we didn't return them as multivals
|
||||
---We have to use or to check for nil and default to interal starting state and param
|
||||
function Iterator:__call(param, state)
|
||||
return self.gen(param or self.param, state or self.state)
|
||||
end
|
||||
|
||||
function Iterator:__tostring()
|
||||
return "<iterator>"
|
||||
end
|
||||
|
||||
-- A special hack for zip/chain to skip last two state, if a wrapped iterator
|
||||
-- has been passed
|
||||
local numargs = function(...)
|
||||
local n = select("#", ...)
|
||||
if n >= 3 then
|
||||
-- Fix last argument
|
||||
local it = select(n - 2, ...)
|
||||
if
|
||||
type(it) == "table"
|
||||
and getmetatable(it) == Iterator
|
||||
and it.param == select(n - 1, ...)
|
||||
and it.state == select(n, ...)
|
||||
then
|
||||
return n - 2
|
||||
end
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
local return_if_not_empty = function(state_x, ...)
|
||||
if state_x == nil then
|
||||
return nil
|
||||
end
|
||||
return ...
|
||||
end
|
||||
|
||||
local call_if_not_empty = function(fun, state_x, ...)
|
||||
if state_x == nil then
|
||||
return nil
|
||||
end
|
||||
return state_x, fun(...)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Basic Functions
|
||||
--------------------------------------------------------------------------------
|
||||
local nil_gen = function(_param, _state)
|
||||
return nil
|
||||
end
|
||||
|
||||
local pairs_gen = pairs {}
|
||||
|
||||
local map_gen = function(map, key)
|
||||
local value
|
||||
key, value = pairs_gen(map, key)
|
||||
return key, key, value
|
||||
end
|
||||
|
||||
local string_gen = function(param, state)
|
||||
state = state + 1
|
||||
if state > #param then
|
||||
return nil
|
||||
end
|
||||
local r = string.sub(param, state, state)
|
||||
return state, r
|
||||
end
|
||||
|
||||
local rawiter = function(obj, param, state)
|
||||
assert(obj ~= nil, "invalid iterator")
|
||||
|
||||
if type(obj) == "table" then
|
||||
local mt = getmetatable(obj)
|
||||
|
||||
if mt ~= nil then
|
||||
if mt == Iterator then
|
||||
return obj.gen, obj.param, obj.state
|
||||
end
|
||||
end
|
||||
|
||||
if compat.islist(obj) then
|
||||
return ipairs(obj)
|
||||
else
|
||||
-- hash
|
||||
return map_gen, obj, nil
|
||||
end
|
||||
elseif type(obj) == "function" then
|
||||
return obj, param, state
|
||||
elseif type(obj) == "string" then
|
||||
if #obj == 0 then
|
||||
return nil_gen, nil, nil
|
||||
end
|
||||
|
||||
return string_gen, obj, 0
|
||||
end
|
||||
|
||||
error(string.format('object %s of type "%s" is not iterable', obj, type(obj)))
|
||||
end
|
||||
|
||||
---Wraps the iterator triplet into a table to allow metamethods and calling with method form
|
||||
---Important! We do not return param and state as multivals like the original luafun
|
||||
---See the __call metamethod for more information
|
||||
---@param gen any
|
||||
---@param param any
|
||||
---@param state any
|
||||
---@return Iterator
|
||||
local function wrap(gen, param, state)
|
||||
return setmetatable({
|
||||
gen = gen,
|
||||
param = param,
|
||||
state = state,
|
||||
}, Iterator)
|
||||
end
|
||||
|
||||
---Unwrap an iterator metatable into the iterator triplet
|
||||
---@param self Iterator
|
||||
---@return any
|
||||
---@return any
|
||||
---@return any
|
||||
local unwrap = function(self)
|
||||
return self.gen, self.param, self.state
|
||||
end
|
||||
|
||||
---Create an iterator from an object
|
||||
---@param obj any
|
||||
---@param param any (optional)
|
||||
---@param state any (optional)
|
||||
---@return Iterator
|
||||
local iter = function(obj, param, state)
|
||||
return wrap(rawiter(obj, param, state))
|
||||
end
|
||||
|
||||
exports.iter = iter
|
||||
exports.wrap = wrap
|
||||
exports.unwrap = unwrap
|
||||
|
||||
function Iterator:for_each(fn)
|
||||
local param, state = self.param, self.state
|
||||
repeat
|
||||
state = call_if_not_empty(fn, self.gen(param, state))
|
||||
until state == nil
|
||||
end
|
||||
|
||||
function Iterator:stateful()
|
||||
return wrap(
|
||||
co.wrap(function()
|
||||
self:for_each(function(...)
|
||||
co.yield(f.first(...), ...)
|
||||
end)
|
||||
|
||||
-- too make sure that we always return nil if there are no more
|
||||
while true do
|
||||
co.yield()
|
||||
end
|
||||
end),
|
||||
nil,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
-- function Iterator:stateful()
|
||||
-- local gen, param, state = self.gen, self.param, self.state
|
||||
|
||||
-- local function return_and_set_state(state_x, ...)
|
||||
-- state = state_x
|
||||
-- if state == nil then return end
|
||||
-- return state_x, ...
|
||||
-- end
|
||||
|
||||
-- local stateful_gen = function()
|
||||
-- return return_and_set_state(gen(param, state))
|
||||
-- end
|
||||
|
||||
-- return wrap(stateful_gen, false, false)
|
||||
-- end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Generators
|
||||
--------------------------------------------------------------------------------
|
||||
local range_gen = function(param, state)
|
||||
local stop, step = param[1], param[2]
|
||||
state = state + step
|
||||
if state > stop then
|
||||
return nil
|
||||
end
|
||||
return state, state
|
||||
end
|
||||
|
||||
local range_rev_gen = function(param, state)
|
||||
local stop, step = param[1], param[2]
|
||||
state = state + step
|
||||
if state < stop then
|
||||
return nil
|
||||
end
|
||||
return state, state
|
||||
end
|
||||
|
||||
---Creates a range iterator
|
||||
---@param start number
|
||||
---@param stop number
|
||||
---@param step number
|
||||
---@return Iterator
|
||||
local range = function(start, stop, step)
|
||||
if step == nil then
|
||||
if stop == nil then
|
||||
if start == 0 then
|
||||
return nil_gen, nil, nil
|
||||
end
|
||||
stop = start
|
||||
start = stop > 0 and 1 or -1
|
||||
end
|
||||
step = start <= stop and 1 or -1
|
||||
end
|
||||
|
||||
assert(type(start) == "number", "start must be a number")
|
||||
assert(type(stop) == "number", "stop must be a number")
|
||||
assert(type(step) == "number", "step must be a number")
|
||||
assert(step ~= 0, "step must not be zero")
|
||||
|
||||
if step > 0 then
|
||||
return wrap(range_gen, { stop, step }, start - step)
|
||||
elseif step < 0 then
|
||||
return wrap(range_rev_gen, { stop, step }, start - step)
|
||||
end
|
||||
end
|
||||
exports.range = range
|
||||
|
||||
local duplicate_table_gen = function(param_x, state_x)
|
||||
return state_x + 1, unpack(param_x)
|
||||
end
|
||||
|
||||
local duplicate_fun_gen = function(param_x, state_x)
|
||||
return state_x + 1, param_x(state_x)
|
||||
end
|
||||
|
||||
local duplicate_gen = function(param_x, state_x)
|
||||
return state_x + 1, param_x
|
||||
end
|
||||
|
||||
---Creates an infinite iterator that will yield the arguments
|
||||
---If multiple arguments are passed, the args will be packed and unpacked
|
||||
---@param ...: the arguments to duplicate
|
||||
---@return Iterator
|
||||
local duplicate = function(...)
|
||||
if select("#", ...) <= 1 then
|
||||
return wrap(duplicate_gen, select(1, ...), 0)
|
||||
else
|
||||
return wrap(duplicate_table_gen, { ... }, 0)
|
||||
end
|
||||
end
|
||||
exports.duplicate = duplicate
|
||||
|
||||
---Creates an iterator from a function
|
||||
---NOTE: if the function is a closure and modifies state, the resulting iterator will not be stateless
|
||||
---@param fun function
|
||||
---@return Iterator
|
||||
local from_fun = function(fun)
|
||||
assert(type(fun) == "function")
|
||||
return wrap(duplicate_fun_gen, fun, 0)
|
||||
end
|
||||
exports.from_fun = from_fun
|
||||
|
||||
---Creates an infinite iterator that will yield zeros.
|
||||
---This is an alias to calling duplicate(0)
|
||||
---@return Iterator
|
||||
local zeros = function()
|
||||
return wrap(duplicate_gen, 0, 0)
|
||||
end
|
||||
exports.zeros = zeros
|
||||
|
||||
---Creates an infinite iterator that will yield ones.
|
||||
---This is an alias to calling duplicate(1)
|
||||
---@return Iterator
|
||||
local ones = function()
|
||||
return wrap(duplicate_gen, 1, 0)
|
||||
end
|
||||
exports.ones = ones
|
||||
|
||||
local rands_gen = function(param_x, _state_x)
|
||||
return 0, math.random(param_x[1], param_x[2])
|
||||
end
|
||||
|
||||
local rands_nil_gen = function(_param_x, _state_x)
|
||||
return 0, math.random()
|
||||
end
|
||||
|
||||
---Creates an infinite iterator that will yield random values.
|
||||
---@param n number
|
||||
---@param m number
|
||||
---@return Iterator
|
||||
local rands = function(n, m)
|
||||
if n == nil and m == nil then
|
||||
return wrap(rands_nil_gen, 0, 0)
|
||||
end
|
||||
assert(type(n) == "number", "invalid first arg to rands")
|
||||
if m == nil then
|
||||
m = n
|
||||
n = 0
|
||||
else
|
||||
assert(type(m) == "number", "invalid second arg to rands")
|
||||
end
|
||||
assert(n < m, "empty interval")
|
||||
return wrap(rands_gen, { n, m - 1 }, 0)
|
||||
end
|
||||
exports.rands = rands
|
||||
|
||||
local split_gen = function(param, state)
|
||||
local input, sep = param[1], param[2]
|
||||
local input_len = #input
|
||||
|
||||
if state > input_len + 1 then
|
||||
return
|
||||
end
|
||||
|
||||
local start, finish = string.find(input, sep, state, true)
|
||||
if not start then
|
||||
start = input_len + 1
|
||||
finish = input_len + 1
|
||||
end
|
||||
|
||||
local sub_str = input:sub(state, start - 1)
|
||||
|
||||
return finish + 1, sub_str
|
||||
end
|
||||
|
||||
---Return an iterator of substrings separated by a string
|
||||
---@param input string: the string to split
|
||||
---@param sep string: the separator to find and split based on
|
||||
---@return Iterator
|
||||
local split = function(input, sep)
|
||||
return wrap(split_gen, { input, sep }, 1)
|
||||
end
|
||||
exports.split = split
|
||||
|
||||
---Splits a string based on a single space
|
||||
---An alias for split(input, " ")
|
||||
---@param input any
|
||||
---@return any
|
||||
local words = function(input)
|
||||
return split(input, " ")
|
||||
end
|
||||
exports.words = words
|
||||
|
||||
local lines = function(input)
|
||||
-- TODO: platform specific linebreaks
|
||||
return split(input, "\n")
|
||||
end
|
||||
exports.lines = lines
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Transformations
|
||||
--------------------------------------------------------------------------------
|
||||
local map_gen2 = function(param, state)
|
||||
local gen_x, param_x, fun = param[1], param[2], param[3]
|
||||
return call_if_not_empty(fun, gen_x(param_x, state))
|
||||
end
|
||||
|
||||
---Iterator adapter that maps the previous iterator with a function
|
||||
---@param fun function: The function to map with. Will be called on each element
|
||||
---@return Iterator
|
||||
function Iterator:map(fun)
|
||||
return wrap(map_gen2, { self.gen, self.param, fun }, self.state)
|
||||
end
|
||||
|
||||
local flatten_gen1
|
||||
do
|
||||
local it = function(new_iter, state_x, ...)
|
||||
if state_x == nil then
|
||||
return nil
|
||||
end
|
||||
return { new_iter.gen, new_iter.param, state_x }, ...
|
||||
end
|
||||
|
||||
flatten_gen1 = function(state, state_x, ...)
|
||||
if state_x == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local first_arg = f.first(...)
|
||||
|
||||
-- experimental part
|
||||
if getmetatable(first_arg) == Iterator then
|
||||
-- attach the iterator to the rest
|
||||
local new_iter = (first_arg .. wrap(state[1], state[2], state_x)):flatten()
|
||||
-- advance the iterator by one
|
||||
return it(new_iter, new_iter.gen(new_iter.param, new_iter.state))
|
||||
end
|
||||
|
||||
return { state[1], state[2], state_x }, ...
|
||||
end
|
||||
end
|
||||
|
||||
local flatten_gen = function(_, state)
|
||||
if state == nil then
|
||||
return
|
||||
end
|
||||
local gen_x, param_x, state_x = state[1], state[2], state[3]
|
||||
return flatten_gen1(state, gen_x(param_x, state_x))
|
||||
end
|
||||
|
||||
---Iterator adapter that will recursivley flatten nested iterator structure
|
||||
---@return Iterator
|
||||
function Iterator:flatten()
|
||||
return wrap(flatten_gen, false, { self.gen, self.param, self.state })
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Filtering
|
||||
--------------------------------------------------------------------------------
|
||||
local filter1_gen = function(fun, gen_x, param_x, state_x, a)
|
||||
while true do
|
||||
if state_x == nil or fun(a) then
|
||||
break
|
||||
end
|
||||
state_x, a = gen_x(param_x, state_x)
|
||||
end
|
||||
return state_x, a
|
||||
end
|
||||
|
||||
-- call each other
|
||||
-- because we can't assign a vararg mutably in a while loop like filter1_gen
|
||||
-- so we have to use recursion in calling both of these functions
|
||||
local filterm_gen
|
||||
local filterm_gen_shrink = function(fun, gen_x, param_x, state_x)
|
||||
return filterm_gen(fun, gen_x, param_x, gen_x(param_x, state_x))
|
||||
end
|
||||
|
||||
filterm_gen = function(fun, gen_x, param_x, state_x, ...)
|
||||
if state_x == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
if fun(...) then
|
||||
return state_x, ...
|
||||
end
|
||||
|
||||
return filterm_gen_shrink(fun, gen_x, param_x, state_x)
|
||||
end
|
||||
|
||||
local filter_detect = function(fun, gen_x, param_x, state_x, ...)
|
||||
if select("#", ...) < 2 then
|
||||
return filter1_gen(fun, gen_x, param_x, state_x, ...)
|
||||
else
|
||||
return filterm_gen(fun, gen_x, param_x, state_x, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local filter_gen = function(param, state_x)
|
||||
local gen_x, param_x, fun = param[1], param[2], param[3]
|
||||
return filter_detect(fun, gen_x, param_x, gen_x(param_x, state_x))
|
||||
end
|
||||
|
||||
---Iterator adapter that will filter values
|
||||
---@param fun function: The function to filter values with. If the function returns true, the value will be kept.
|
||||
---@return Iterator
|
||||
function Iterator:filter(fun)
|
||||
return wrap(filter_gen, { self.gen, self.param, fun }, self.state)
|
||||
end
|
||||
|
||||
---Iterator adapter that will provide numbers from 1 to n as the first multival
|
||||
---@return Iterator
|
||||
function Iterator:enumerate()
|
||||
local i = 0
|
||||
return self:map(function(...)
|
||||
i = i + 1
|
||||
return i, ...
|
||||
end)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Reducing
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
---Returns true if any of the values in the iterator satisfy a predicate
|
||||
---@param fun function
|
||||
---@return boolean
|
||||
function Iterator:any(fun)
|
||||
local r
|
||||
local state, param, gen = self.state, self.param, self.gen
|
||||
repeat
|
||||
state, r = call_if_not_empty(fun, gen(param, state))
|
||||
until state == nil or r
|
||||
return r
|
||||
end
|
||||
|
||||
---Returns true if all of the values in the iterator satisfy a predicate
|
||||
---@param fun function
|
||||
---@return boolean
|
||||
function Iterator:all(fun)
|
||||
local r
|
||||
local state, param, gen = self.state, self.param, self.gen
|
||||
repeat
|
||||
state, r = call_if_not_empty(fun, gen(param, state))
|
||||
until state == nil or not r
|
||||
return state == nil
|
||||
end
|
||||
|
||||
---Finds a value that is equal to the provided value of satisfies a predicate.
|
||||
---@param val_or_fn any
|
||||
---@return any
|
||||
function Iterator:find(val_or_fn)
|
||||
local gen, param, state = self.gen, self.param, self.state
|
||||
if type(val_or_fn) == "function" then
|
||||
return return_if_not_empty(filter_detect(val_or_fn, gen, param, gen(param, state)))
|
||||
else
|
||||
for _, r in gen, param, state do
|
||||
if r == val_or_fn then
|
||||
return r
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
---Folds an iterator into a single value using a function.
|
||||
---@param init any
|
||||
---@param fun fun(acc: any, val: any): any
|
||||
---@return any
|
||||
function Iterator:fold(init, fun)
|
||||
local acc = init
|
||||
local gen, param, state = self.gen, self.param, self.state
|
||||
for _, r in gen, param, state do
|
||||
acc = fun(acc, r)
|
||||
end
|
||||
return acc
|
||||
end
|
||||
|
||||
---Turns an iterator into a list.
|
||||
---If the iterator yields multivals only the first multival will be used.
|
||||
---@return table
|
||||
function Iterator:tolist()
|
||||
local list = {}
|
||||
self:for_each(function(a)
|
||||
table.insert(list, a)
|
||||
end)
|
||||
return list
|
||||
end
|
||||
|
||||
---Turns an iterator into a list.
|
||||
---If the iterator yields multivals all multivals will be used and packed into a table.
|
||||
---@return table
|
||||
function Iterator:tolistn()
|
||||
local list = {}
|
||||
self:for_each(function(...)
|
||||
table.insert(list, { ... })
|
||||
end)
|
||||
return list
|
||||
end
|
||||
|
||||
---Turns an iterator into a map.
|
||||
---The first multival that the iterator yields will be the key.
|
||||
---The second multival that the iterator yields will be the value.
|
||||
---@return table
|
||||
function Iterator:tomap()
|
||||
local map = {}
|
||||
self:for_each(function(key, value)
|
||||
map[key] = value
|
||||
end)
|
||||
return map
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Compositions
|
||||
--------------------------------------------------------------------------------
|
||||
-- call each other
|
||||
local chain_gen_r1
|
||||
local chain_gen_r2 = function(param, state, state_x, ...)
|
||||
if state_x == nil then
|
||||
local i = state[1] + 1
|
||||
if param[3 * i - 1] == nil then
|
||||
return nil
|
||||
end
|
||||
state_x = param[3 * i]
|
||||
return chain_gen_r1(param, { i, state_x })
|
||||
end
|
||||
return { state[1], state_x }, ...
|
||||
end
|
||||
|
||||
chain_gen_r1 = function(param, state)
|
||||
local i, state_x = state[1], state[2]
|
||||
local gen_x, param_x = param[3 * i - 2], param[3 * i - 1]
|
||||
return chain_gen_r2(param, state, gen_x(param_x, state_x))
|
||||
end
|
||||
|
||||
---Make an iterator that returns elements from the first iterator until it is exhausted,
|
||||
---then proceeds to the next iterator,
|
||||
---until all of the iterators are exhausted.
|
||||
---Used for treating consecutive iterators as a single iterator.
|
||||
---Infinity iterators are supported, but are not recommended.
|
||||
---@param ...: the iterators to chain
|
||||
---@return Iterator
|
||||
local chain = function(...)
|
||||
local n = numargs(...)
|
||||
|
||||
if n == 0 then
|
||||
return wrap(nil_gen, nil, nil)
|
||||
end
|
||||
|
||||
local param = { [3 * n] = 0 }
|
||||
|
||||
local gen_x, param_x, state_x
|
||||
for i = 1, n, 1 do
|
||||
local elem = select(i, ...)
|
||||
gen_x, param_x, state_x = unwrap(elem)
|
||||
param[3 * i - 2] = gen_x
|
||||
param[3 * i - 1] = param_x
|
||||
param[3 * i] = state_x
|
||||
end
|
||||
|
||||
return wrap(chain_gen_r1, param, { 1, param[3] })
|
||||
end
|
||||
|
||||
Iterator.chain = chain
|
||||
Iterator.__concat = chain
|
||||
exports.chain = chain
|
||||
|
||||
local function zip_gen_r(param, state, state_new, ...)
|
||||
if #state_new == #param / 2 then
|
||||
return state_new, ...
|
||||
end
|
||||
|
||||
local i = #state_new + 1
|
||||
local gen_x, param_x = param[2 * i - 1], param[2 * i]
|
||||
local state_x, r = gen_x(param_x, state[i])
|
||||
if state_x == nil then
|
||||
return nil
|
||||
end
|
||||
table.insert(state_new, state_x)
|
||||
return zip_gen_r(param, state, state_new, r, ...)
|
||||
end
|
||||
|
||||
local zip_gen = function(param, state)
|
||||
return zip_gen_r(param, state, {})
|
||||
end
|
||||
|
||||
---Return a new iterator where i-th return value contains the i-th element from each of the iterators.
|
||||
---The returned iterator is truncated in length to the length of the shortest iterator.
|
||||
---For multi-return iterators only the first variable is used.
|
||||
---@param ...: the iterators to zip
|
||||
---@return Iterator
|
||||
local zip = function(...)
|
||||
local n = numargs(...)
|
||||
if n == 0 then
|
||||
return wrap(nil_gen, nil, nil)
|
||||
end
|
||||
local param = { [2 * n] = 0 }
|
||||
local state = { [n] = 0 }
|
||||
|
||||
local gen_x, param_x, state_x
|
||||
for i = 1, n, 1 do
|
||||
local it = select(n - i + 1, ...)
|
||||
gen_x, param_x, state_x = rawiter(it)
|
||||
param[2 * i - 1] = gen_x
|
||||
param[2 * i] = param_x
|
||||
state[i] = state_x
|
||||
end
|
||||
|
||||
return wrap(zip_gen, param, state)
|
||||
end
|
||||
|
||||
Iterator.zip = zip
|
||||
Iterator.__div = zip
|
||||
exports.zip = zip
|
||||
|
||||
return exports
|
||||
678
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/job.lua
Normal file
678
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/job.lua
Normal file
|
|
@ -0,0 +1,678 @@
|
|||
local vim = vim
|
||||
local uv = vim.loop
|
||||
local compat = require "plenary.compat"
|
||||
|
||||
local F = require "plenary.functional"
|
||||
|
||||
---@class Job
|
||||
---@field command string Command to run
|
||||
---@field args? string[] List of arguments to pass
|
||||
---@field cwd? string Working directory for job
|
||||
---@field env? table<string, string>|string[] Environment looking like: { ['VAR'] = 'VALUE' } or { 'VAR=VALUE' }
|
||||
---@field interactive? boolean
|
||||
---@field detached? boolean Spawn the child in a detached state making it a process group leader
|
||||
---@field skip_validation? boolean Skip validating the arguments
|
||||
---@field enable_handlers? boolean If set to false, disables all callbacks associated with output (default: true)
|
||||
---@field enable_recording? boolean
|
||||
---@field on_start? fun()
|
||||
---@field on_stdout? fun(error: string, data: string, self?: Job)
|
||||
---@field on_stderr? fun(error: string, data: string, self?: Job)
|
||||
---@field on_exit? fun(self: Job, code: number, signal: number)
|
||||
---@field maximum_results? number Stop processing results after this number
|
||||
---@field writer? Job|table|string Job that writes to stdin of this job.
|
||||
local Job = {}
|
||||
Job.__index = Job
|
||||
|
||||
local function close_safely(j, key)
|
||||
local handle = j[key]
|
||||
|
||||
if not handle then
|
||||
return
|
||||
end
|
||||
|
||||
if not handle:is_closing() then
|
||||
handle:close()
|
||||
end
|
||||
end
|
||||
|
||||
local start_shutdown_check = function(child, options, code, signal)
|
||||
uv.check_start(child._shutdown_check, function()
|
||||
if not child:_pipes_are_closed(options) then
|
||||
return
|
||||
end
|
||||
|
||||
-- Wait until all the pipes are closing.
|
||||
uv.check_stop(child._shutdown_check)
|
||||
child._shutdown_check = nil
|
||||
|
||||
child:_shutdown(code, signal)
|
||||
|
||||
-- Remove left over references
|
||||
child = nil
|
||||
end)
|
||||
end
|
||||
|
||||
local shutdown_factory = function(child, options)
|
||||
return function(code, signal)
|
||||
if uv.is_closing(child._shutdown_check) then
|
||||
return child:shutdown(code, signal)
|
||||
else
|
||||
start_shutdown_check(child, options, code, signal)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function expand(path)
|
||||
if vim.in_fast_event() then
|
||||
return assert(uv.fs_realpath(path), string.format("Path must be valid: %s", path))
|
||||
else
|
||||
-- TODO: Probably want to check that this is valid here... otherwise that's weird.
|
||||
return vim.fn.expand(vim.fn.escape(path, "[]$"), true)
|
||||
end
|
||||
end
|
||||
|
||||
---@class Array
|
||||
--- Numeric table
|
||||
|
||||
---@class Map
|
||||
--- Map-like table
|
||||
|
||||
---Create a new job
|
||||
---@param o Job
|
||||
---@return Job
|
||||
function Job:new(o)
|
||||
if not o then
|
||||
error(debug.traceback "Options are required for Job:new")
|
||||
end
|
||||
|
||||
local command = o.command
|
||||
if not command then
|
||||
if o[1] then
|
||||
command = o[1]
|
||||
else
|
||||
error(debug.traceback "'command' is required for Job:new")
|
||||
end
|
||||
elseif o[1] then
|
||||
error(debug.traceback "Cannot pass both 'command' and array args")
|
||||
end
|
||||
|
||||
local args = o.args
|
||||
if not args then
|
||||
if #o > 1 then
|
||||
args = { select(2, unpack(o)) }
|
||||
end
|
||||
end
|
||||
|
||||
local ok, is_exe = pcall(vim.fn.executable, command)
|
||||
if not o.skip_validation and ok and 1 ~= is_exe then
|
||||
error(debug.traceback(command .. ": Executable not found"))
|
||||
end
|
||||
|
||||
local obj = {}
|
||||
|
||||
obj.command = command
|
||||
obj.args = args
|
||||
obj._raw_cwd = o.cwd
|
||||
if o.env then
|
||||
if type(o.env) ~= "table" then
|
||||
error "[plenary.job] env has to be a table"
|
||||
end
|
||||
|
||||
local transform = {}
|
||||
for k, v in pairs(o.env) do
|
||||
if type(k) == "number" then
|
||||
table.insert(transform, v)
|
||||
elseif type(k) == "string" then
|
||||
table.insert(transform, k .. "=" .. tostring(v))
|
||||
end
|
||||
end
|
||||
obj.env = transform
|
||||
end
|
||||
if o.interactive == nil then
|
||||
obj.interactive = true
|
||||
else
|
||||
obj.interactive = o.interactive
|
||||
end
|
||||
|
||||
if o.detached then
|
||||
obj.detached = true
|
||||
end
|
||||
|
||||
-- enable_handlers: Do you want to do ANYTHING with the stdout/stderr of the proc
|
||||
obj.enable_handlers = F.if_nil(o.enable_handlers, true, o.enable_handlers)
|
||||
|
||||
-- enable_recording: Do you want to record stdout/stderr into a table.
|
||||
-- Since it cannot be enabled when enable_handlers is false,
|
||||
-- we try and make sure they are associated correctly.
|
||||
obj.enable_recording =
|
||||
F.if_nil(F.if_nil(o.enable_recording, o.enable_handlers, o.enable_recording), true, o.enable_recording)
|
||||
|
||||
if not obj.enable_handlers and obj.enable_recording then
|
||||
error "[plenary.job] Cannot record items but disable handlers"
|
||||
end
|
||||
|
||||
obj._user_on_start = o.on_start
|
||||
obj._user_on_stdout = o.on_stdout
|
||||
obj._user_on_stderr = o.on_stderr
|
||||
obj._user_on_exit = o.on_exit
|
||||
|
||||
obj._additional_on_exit_callbacks = {}
|
||||
|
||||
obj._maximum_results = o.maximum_results
|
||||
|
||||
obj.user_data = {}
|
||||
|
||||
obj.writer = o.writer
|
||||
|
||||
self._reset(obj)
|
||||
|
||||
return setmetatable(obj, self)
|
||||
end
|
||||
|
||||
function Job:_reset()
|
||||
self.is_shutdown = nil
|
||||
|
||||
if self._shutdown_check and uv.is_active(self._shutdown_check) and not uv.is_closing(self._shutdown_check) then
|
||||
vim.api.nvim_err_writeln(debug.traceback "We may be memory leaking here. Please report to TJ.")
|
||||
end
|
||||
self._shutdown_check = uv.new_check()
|
||||
|
||||
self.stdin = nil
|
||||
self.stdout = nil
|
||||
self.stderr = nil
|
||||
|
||||
self._stdout_reader = nil
|
||||
self._stderr_reader = nil
|
||||
|
||||
if self.enable_recording then
|
||||
self._stdout_results = {}
|
||||
self._stderr_results = {}
|
||||
else
|
||||
self._stdout_results = nil
|
||||
self._stderr_results = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Stop a job and close all handles
|
||||
function Job:_stop()
|
||||
close_safely(self, "stdin")
|
||||
close_safely(self, "stderr")
|
||||
close_safely(self, "stdout")
|
||||
close_safely(self, "handle")
|
||||
end
|
||||
|
||||
function Job:_pipes_are_closed(options)
|
||||
for _, pipe in ipairs { options.stdin, options.stdout, options.stderr } do
|
||||
if pipe and not uv.is_closing(pipe) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Shutdown a job.
|
||||
function Job:shutdown(code, signal)
|
||||
if self._shutdown_check and uv.is_active(self._shutdown_check) then
|
||||
-- shutdown has already started
|
||||
return
|
||||
end
|
||||
|
||||
self:_shutdown(code, signal)
|
||||
end
|
||||
|
||||
function Job:_shutdown(code, signal)
|
||||
if self.is_shutdown then
|
||||
return
|
||||
end
|
||||
|
||||
self.code = code
|
||||
self.signal = signal
|
||||
|
||||
if self._stdout_reader then
|
||||
pcall(self._stdout_reader, nil, nil, true)
|
||||
end
|
||||
|
||||
if self._stderr_reader then
|
||||
pcall(self._stderr_reader, nil, nil, true)
|
||||
end
|
||||
|
||||
if self._user_on_exit then
|
||||
self:_user_on_exit(code, signal)
|
||||
end
|
||||
|
||||
for _, v in ipairs(self._additional_on_exit_callbacks) do
|
||||
v(self, code, signal)
|
||||
end
|
||||
|
||||
if self.stdout then
|
||||
self.stdout:read_stop()
|
||||
end
|
||||
|
||||
if self.stderr then
|
||||
self.stderr:read_stop()
|
||||
end
|
||||
|
||||
self:_stop()
|
||||
|
||||
self.is_shutdown = true
|
||||
|
||||
self._stdout_reader = nil
|
||||
self._stderr_reader = nil
|
||||
end
|
||||
|
||||
function Job:_create_uv_options()
|
||||
local options = {}
|
||||
|
||||
options.command = self.command
|
||||
options.args = self.args
|
||||
options.stdio = { self.stdin, self.stdout, self.stderr }
|
||||
|
||||
if self._raw_cwd then
|
||||
options.cwd = expand(self._raw_cwd)
|
||||
end
|
||||
if self.env then
|
||||
options.env = self.env
|
||||
end
|
||||
|
||||
if self.detached then
|
||||
options.detached = true
|
||||
end
|
||||
|
||||
return options
|
||||
end
|
||||
|
||||
local on_output = function(self, result_key, cb)
|
||||
return coroutine.wrap(function(err, data, is_complete)
|
||||
local result_index = 1
|
||||
|
||||
local line, start, result_line, found_newline
|
||||
|
||||
-- We repeat forever as a coroutine so that we can keep calling this.
|
||||
while true do
|
||||
if data then
|
||||
data = data:gsub("\r", "")
|
||||
|
||||
local processed_index = 1
|
||||
local data_length = #data + 1
|
||||
|
||||
repeat
|
||||
start = string.find(data, "\n", processed_index, true) or data_length
|
||||
line = string.sub(data, processed_index, start - 1)
|
||||
found_newline = start ~= data_length
|
||||
|
||||
-- Concat to last line if there was something there already.
|
||||
-- This happens when "data" is broken into chunks and sometimes
|
||||
-- the content is sent without any newlines.
|
||||
if result_line then
|
||||
-- results[result_index] = results[result_index] .. line
|
||||
result_line = result_line .. line
|
||||
|
||||
-- Only put in a new line when we actually have new data to split.
|
||||
-- This is generally only false when we do end with a new line.
|
||||
-- It prevents putting in a "" to the end of the results.
|
||||
elseif start ~= processed_index or found_newline then
|
||||
-- results[result_index] = line
|
||||
result_line = line
|
||||
|
||||
-- Otherwise, we don't need to do anything.
|
||||
end
|
||||
|
||||
if found_newline then
|
||||
if not result_line then
|
||||
return vim.api.nvim_err_writeln(
|
||||
"Broken data thing due to: " .. tostring(result_line) .. " " .. tostring(data)
|
||||
)
|
||||
end
|
||||
|
||||
if self.enable_recording then
|
||||
self[result_key][result_index] = result_line
|
||||
end
|
||||
|
||||
if cb then
|
||||
cb(err, result_line, self)
|
||||
end
|
||||
|
||||
-- Stop processing if we've surpassed the maximum.
|
||||
if self._maximum_results and result_index > self._maximum_results then
|
||||
-- Shutdown once we get the chance.
|
||||
-- Can't call it here, because we'll just keep calling ourselves.
|
||||
vim.schedule(function()
|
||||
self:shutdown()
|
||||
end)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
result_index = result_index + 1
|
||||
result_line = nil
|
||||
end
|
||||
|
||||
processed_index = start + 1
|
||||
until not found_newline
|
||||
end
|
||||
|
||||
if self.enable_recording then
|
||||
self[result_key][result_index] = result_line
|
||||
end
|
||||
|
||||
-- If we didn't get a newline on the last execute, send the final results.
|
||||
if cb and is_complete and not found_newline then
|
||||
cb(err, result_line, self)
|
||||
end
|
||||
|
||||
if is_complete then
|
||||
return
|
||||
end
|
||||
|
||||
err, data, is_complete = coroutine.yield()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Stop previous execution and add new pipes.
|
||||
--- Also regenerates pipes of writer.
|
||||
function Job:_prepare_pipes()
|
||||
self:_stop()
|
||||
|
||||
if self.writer then
|
||||
if Job.is_job(self.writer) then
|
||||
self.writer:_prepare_pipes()
|
||||
self.stdin = self.writer.stdout
|
||||
elseif self.writer.write then
|
||||
self.stdin = self.writer
|
||||
end
|
||||
end
|
||||
|
||||
if not self.stdin then
|
||||
self.stdin = self.interactive and uv.new_pipe(false) or nil
|
||||
end
|
||||
|
||||
self.stdout = uv.new_pipe(false)
|
||||
self.stderr = uv.new_pipe(false)
|
||||
end
|
||||
|
||||
--- Execute job. Should be called only after preprocessing is done.
|
||||
function Job:_execute()
|
||||
local options = self:_create_uv_options()
|
||||
|
||||
if self._user_on_start then
|
||||
self:_user_on_start()
|
||||
end
|
||||
|
||||
self.handle, self.pid = uv.spawn(options.command, options, shutdown_factory(self, options))
|
||||
|
||||
if not self.handle then
|
||||
error(debug.traceback("Failed to spawn process: " .. vim.inspect(self)))
|
||||
end
|
||||
|
||||
if self.enable_handlers then
|
||||
self._stdout_reader = on_output(self, "_stdout_results", self._user_on_stdout)
|
||||
self.stdout:read_start(self._stdout_reader)
|
||||
|
||||
self._stderr_reader = on_output(self, "_stderr_results", self._user_on_stderr)
|
||||
self.stderr:read_start(self._stderr_reader)
|
||||
end
|
||||
|
||||
if self.writer then
|
||||
if Job.is_job(self.writer) then
|
||||
self.writer:_execute()
|
||||
elseif type(self.writer) == "table" and compat.islist(self.writer) then
|
||||
local writer_len = #self.writer
|
||||
for i, v in ipairs(self.writer) do
|
||||
self.stdin:write(v)
|
||||
if i ~= writer_len then
|
||||
self.stdin:write "\n"
|
||||
else
|
||||
self.stdin:write("\n", function()
|
||||
pcall(self.stdin.close, self.stdin)
|
||||
end)
|
||||
end
|
||||
end
|
||||
elseif type(self.writer) == "string" then
|
||||
self.stdin:write(self.writer, function()
|
||||
self.stdin:close()
|
||||
end)
|
||||
elseif self.writer.write then
|
||||
self.stdin = self.writer
|
||||
else
|
||||
error("Unknown self.writer: " .. vim.inspect(self.writer))
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Job:start()
|
||||
self:_reset()
|
||||
self:_prepare_pipes()
|
||||
self:_execute()
|
||||
end
|
||||
|
||||
function Job:sync(timeout, wait_interval)
|
||||
self:start()
|
||||
self:wait(timeout, wait_interval)
|
||||
|
||||
return self.enable_recording and self:result() or nil, self.code
|
||||
end
|
||||
|
||||
function Job:result()
|
||||
assert(self.enable_recording, "'enable_recording' is not enabled for this job.")
|
||||
return self._stdout_results
|
||||
end
|
||||
|
||||
function Job:stderr_result()
|
||||
assert(self.enable_recording, "'enable_recording' is not enabled for this job.")
|
||||
return self._stderr_results
|
||||
end
|
||||
|
||||
function Job:pid()
|
||||
return self.pid
|
||||
end
|
||||
|
||||
function Job:wait(timeout, wait_interval, should_redraw)
|
||||
timeout = timeout or 5000
|
||||
wait_interval = wait_interval or 10
|
||||
|
||||
if self.handle == nil then
|
||||
local msg = vim.inspect(self)
|
||||
vim.schedule(function()
|
||||
vim.api.nvim_err_writeln(msg)
|
||||
end)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
-- Wait five seconds, or until timeout.
|
||||
local wait_result = vim.wait(timeout, function()
|
||||
if should_redraw then
|
||||
vim.cmd [[redraw!]]
|
||||
end
|
||||
|
||||
if self.is_shutdown then
|
||||
assert(not self.handle or self.handle:is_closing(), "Job must be shutdown if it's closing")
|
||||
end
|
||||
|
||||
return self.is_shutdown
|
||||
end, wait_interval, not should_redraw)
|
||||
|
||||
if not wait_result then
|
||||
error(
|
||||
string.format(
|
||||
"'%s %s' was unable to complete in %s ms",
|
||||
self.command,
|
||||
table.concat(self.args or {}, " "),
|
||||
timeout
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Job:co_wait(wait_time)
|
||||
wait_time = wait_time or 5
|
||||
|
||||
if self.handle == nil then
|
||||
vim.api.nvim_err_writeln(vim.inspect(self))
|
||||
return
|
||||
end
|
||||
|
||||
while not vim.wait(wait_time, function()
|
||||
return self.is_shutdown
|
||||
end) do
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Wait for all jobs to complete
|
||||
function Job.join(...)
|
||||
local jobs_to_wait = { ... }
|
||||
local num_jobs = table.getn(jobs_to_wait)
|
||||
|
||||
-- last entry can be timeout
|
||||
local timeout
|
||||
if type(jobs_to_wait[num_jobs]) == "number" then
|
||||
timeout = table.remove(jobs_to_wait, num_jobs)
|
||||
num_jobs = num_jobs - 1
|
||||
end
|
||||
|
||||
local completed = 0
|
||||
|
||||
return vim.wait(timeout or 10000, function()
|
||||
for index, current_job in pairs(jobs_to_wait) do
|
||||
if current_job.is_shutdown then
|
||||
jobs_to_wait[index] = nil
|
||||
completed = completed + 1
|
||||
end
|
||||
end
|
||||
|
||||
return num_jobs == completed
|
||||
end)
|
||||
end
|
||||
|
||||
local _request_id = 0
|
||||
local _request_status = {}
|
||||
|
||||
function Job:and_then(next_job)
|
||||
self:add_on_exit_callback(function()
|
||||
next_job:start()
|
||||
end)
|
||||
end
|
||||
|
||||
function Job:and_then_wrap(next_job)
|
||||
self:add_on_exit_callback(vim.schedule_wrap(function()
|
||||
next_job:start()
|
||||
end))
|
||||
end
|
||||
|
||||
function Job:after(fn)
|
||||
self:add_on_exit_callback(fn)
|
||||
return self
|
||||
end
|
||||
|
||||
function Job:and_then_on_success(next_job)
|
||||
self:add_on_exit_callback(function(_, code)
|
||||
if code == 0 then
|
||||
next_job:start()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Job:and_then_on_success_wrap(next_job)
|
||||
self:add_on_exit_callback(vim.schedule_wrap(function(_, code)
|
||||
if code == 0 then
|
||||
next_job:start()
|
||||
end
|
||||
end))
|
||||
end
|
||||
|
||||
function Job:after_success(fn)
|
||||
self:add_on_exit_callback(function(j, code, signal)
|
||||
if code == 0 then
|
||||
fn(j, code, signal)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Job:and_then_on_failure(next_job)
|
||||
self:add_on_exit_callback(function(_, code)
|
||||
if code ~= 0 then
|
||||
next_job:start()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Job:and_then_on_failure_wrap(next_job)
|
||||
self:add_on_exit_callback(vim.schedule_wrap(function(_, code)
|
||||
if code ~= 0 then
|
||||
next_job:start()
|
||||
end
|
||||
end))
|
||||
end
|
||||
|
||||
function Job:after_failure(fn)
|
||||
self:add_on_exit_callback(function(j, code, signal)
|
||||
if code ~= 0 then
|
||||
fn(j, code, signal)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Job.chain(...)
|
||||
_request_id = _request_id + 1
|
||||
_request_status[_request_id] = false
|
||||
|
||||
local jobs = { ... }
|
||||
|
||||
for index = 2, #jobs do
|
||||
local prev_job = jobs[index - 1]
|
||||
local job = jobs[index]
|
||||
|
||||
prev_job:add_on_exit_callback(vim.schedule_wrap(function()
|
||||
job:start()
|
||||
end))
|
||||
end
|
||||
|
||||
local last_on_exit = jobs[#jobs]._user_on_exit
|
||||
jobs[#jobs]._user_on_exit = function(self, err, data)
|
||||
if last_on_exit then
|
||||
last_on_exit(self, err, data)
|
||||
end
|
||||
|
||||
_request_status[_request_id] = true
|
||||
end
|
||||
|
||||
jobs[1]:start()
|
||||
|
||||
return _request_id
|
||||
end
|
||||
|
||||
function Job.chain_status(id)
|
||||
return _request_status[id]
|
||||
end
|
||||
|
||||
function Job.is_job(item)
|
||||
if type(item) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
return getmetatable(item) == Job
|
||||
end
|
||||
|
||||
function Job:add_on_exit_callback(cb)
|
||||
table.insert(self._additional_on_exit_callbacks, cb)
|
||||
end
|
||||
|
||||
--- Send data to a job.
|
||||
function Job:send(data)
|
||||
if not self.stdin then
|
||||
error "job has no 'stdin'. Have you run `job:start()` yet?"
|
||||
end
|
||||
|
||||
self.stdin:write(data)
|
||||
end
|
||||
|
||||
return Job
|
||||
116
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/json.lua
Normal file
116
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/json.lua
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
-- based on https://github.com/sindresorhus/strip-json-comments
|
||||
|
||||
local singleComment = "singleComment"
|
||||
local multiComment = "multiComment"
|
||||
local stripWithoutWhitespace = function()
|
||||
return ""
|
||||
end
|
||||
|
||||
local function slice(str, from, to)
|
||||
from = from or 1
|
||||
to = to or #str
|
||||
return str:sub(from, to)
|
||||
end
|
||||
|
||||
local stripWithWhitespace = function(str, from, to)
|
||||
return slice(str, from, to):gsub("%S", " ")
|
||||
end
|
||||
|
||||
local isEscaped = function(jsonString, quotePosition)
|
||||
local index = quotePosition - 1
|
||||
local backslashCount = 0
|
||||
|
||||
while jsonString:sub(index, index) == "\\" do
|
||||
index = index - 1
|
||||
backslashCount = backslashCount + 1
|
||||
end
|
||||
return backslashCount % 2 == 1 and true or false
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Strips any json comments from a json string.
|
||||
-- The resulting string can then be used by `vim.fn.json_decode`
|
||||
--
|
||||
---@param jsonString string
|
||||
---@param options? table
|
||||
--- * whitespace:
|
||||
--- - defaults to true
|
||||
--- - when true, comments will be replaced by whitespace
|
||||
--- - when false, comments will be stripped
|
||||
--- * trailing_commas:
|
||||
--- - defaults to false
|
||||
--- - when true, trailing commas will be included
|
||||
--- - when false, trailing commas will be removed
|
||||
function M.json_strip_comments(jsonString, options)
|
||||
options = options or {}
|
||||
local strip = options.whitespace == false and stripWithoutWhitespace or stripWithWhitespace
|
||||
local omitTrailingCommas = not options.trailing_commas
|
||||
|
||||
local insideString = false
|
||||
local insideComment = false
|
||||
local offset = 1
|
||||
local result = ""
|
||||
local skip = false
|
||||
local lastComma = 0
|
||||
|
||||
for i = 1, #jsonString, 1 do
|
||||
if skip then
|
||||
skip = false
|
||||
else
|
||||
local currentCharacter = jsonString:sub(i, i)
|
||||
local nextCharacter = jsonString:sub(i + 1, i + 1)
|
||||
|
||||
if not insideComment and currentCharacter == '"' then
|
||||
local escaped = isEscaped(jsonString, i)
|
||||
if not escaped then
|
||||
insideString = not insideString
|
||||
end
|
||||
end
|
||||
|
||||
if not insideString then
|
||||
if not insideComment and currentCharacter .. nextCharacter == "//" then
|
||||
result = result .. slice(jsonString, offset, i - 1)
|
||||
offset = i
|
||||
insideComment = singleComment
|
||||
skip = true
|
||||
elseif insideComment == singleComment and currentCharacter .. nextCharacter == "\r\n" then
|
||||
i = i + 1
|
||||
skip = true
|
||||
insideComment = false
|
||||
result = result .. strip(jsonString, offset, i - 1)
|
||||
offset = i
|
||||
elseif insideComment == singleComment and currentCharacter == "\n" then
|
||||
insideComment = false
|
||||
result = result .. strip(jsonString, offset, i - 1)
|
||||
offset = i
|
||||
elseif not insideComment and currentCharacter .. nextCharacter == "/*" then
|
||||
result = result .. slice(jsonString, offset, i - 1)
|
||||
offset = i
|
||||
insideComment = multiComment
|
||||
skip = true
|
||||
elseif insideComment == multiComment and currentCharacter .. nextCharacter == "*/" then
|
||||
i = i + 1
|
||||
skip = true
|
||||
insideComment = false
|
||||
result = result .. strip(jsonString, offset, i)
|
||||
offset = i + 1
|
||||
elseif omitTrailingCommas and not insideComment then
|
||||
if currentCharacter == "," then
|
||||
lastComma = i
|
||||
elseif (currentCharacter == "]" or currentCharacter == "}") and lastComma > 0 then
|
||||
result = result .. slice(jsonString, offset, lastComma - 1) .. slice(jsonString, lastComma + 1, i)
|
||||
offset = i + 1
|
||||
lastComma = 0
|
||||
elseif currentCharacter:match "%S" then
|
||||
lastComma = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result .. (insideComment and strip(slice(jsonString, offset)) or slice(jsonString, offset))
|
||||
end
|
||||
|
||||
return M
|
||||
235
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/log.lua
Normal file
235
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/log.lua
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
-- log.lua
|
||||
-- Does only support logging source files.
|
||||
--
|
||||
-- Inspired by rxi/log.lua
|
||||
-- Modified by tjdevries and can be found at github.com/tjdevries/vlog.nvim
|
||||
--
|
||||
-- This library is free software; you can redistribute it and/or modify it
|
||||
-- under the terms of the MIT license. See LICENSE for details.
|
||||
|
||||
local Path = require "plenary.path"
|
||||
|
||||
local p_debug = vim.fn.getenv "DEBUG_PLENARY"
|
||||
if p_debug == vim.NIL then
|
||||
p_debug = false
|
||||
end
|
||||
|
||||
-- User configuration section
|
||||
local default_config = {
|
||||
-- Name of the plugin. Prepended to log messages.
|
||||
plugin = "plenary",
|
||||
|
||||
-- Should print the output to neovim while running.
|
||||
-- values: 'sync','async',false
|
||||
use_console = "async",
|
||||
|
||||
-- Should highlighting be used in console (using echohl).
|
||||
highlights = true,
|
||||
|
||||
-- Should write to a file.
|
||||
-- Default output for logging file is `stdpath("log")/plugin.log`.
|
||||
use_file = true,
|
||||
|
||||
-- Output file has precedence over plugin, if not nil.
|
||||
-- Used for the logging file, if not nil and use_file == true.
|
||||
outfile = nil,
|
||||
|
||||
-- Should write to the quickfix list.
|
||||
use_quickfix = false,
|
||||
|
||||
-- Any messages above this level will be logged.
|
||||
level = p_debug and "debug" or "info",
|
||||
|
||||
-- Level configuration.
|
||||
modes = {
|
||||
{ name = "trace", hl = "Comment" },
|
||||
{ name = "debug", hl = "Comment" },
|
||||
{ name = "info", hl = "None" },
|
||||
{ name = "warn", hl = "WarningMsg" },
|
||||
{ name = "error", hl = "ErrorMsg" },
|
||||
{ name = "fatal", hl = "ErrorMsg" },
|
||||
},
|
||||
|
||||
-- Can limit the number of decimals displayed for floats.
|
||||
float_precision = 0.01,
|
||||
|
||||
-- Adjust content as needed, but must keep function parameters to be filled
|
||||
-- by library code.
|
||||
---@param is_console boolean If output is for console or log file.
|
||||
---@param mode_name string Level configuration 'modes' field 'name'
|
||||
---@param src_path string Path to source file given by debug.info.source
|
||||
---@param src_line integer Line into source file given by debug.info.currentline
|
||||
---@param msg string Message, which is later on escaped, if needed.
|
||||
fmt_msg = function(is_console, mode_name, src_path, src_line, msg)
|
||||
local nameupper = mode_name:upper()
|
||||
local lineinfo = src_path .. ":" .. src_line
|
||||
if is_console then
|
||||
return string.format("[%-6s%s] %s: %s", nameupper, os.date "%H:%M:%S", lineinfo, msg)
|
||||
else
|
||||
return string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg)
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
-- {{{ NO NEED TO CHANGE
|
||||
local log = {}
|
||||
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
log.new = function(config, standalone)
|
||||
config = vim.tbl_deep_extend("force", default_config, config)
|
||||
|
||||
local outfile = vim.F.if_nil(
|
||||
config.outfile,
|
||||
Path:new(vim.api.nvim_call_function("stdpath", { "log" }), config.plugin .. ".log").filename
|
||||
)
|
||||
|
||||
local obj
|
||||
if standalone then
|
||||
obj = log
|
||||
else
|
||||
obj = config
|
||||
end
|
||||
|
||||
local levels = {}
|
||||
for i, v in ipairs(config.modes) do
|
||||
levels[v.name] = i
|
||||
end
|
||||
|
||||
local round = function(x, increment)
|
||||
if x == 0 then
|
||||
return x
|
||||
end
|
||||
increment = increment or 1
|
||||
x = x / increment
|
||||
return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment
|
||||
end
|
||||
|
||||
local make_string = function(...)
|
||||
local t = {}
|
||||
for i = 1, select("#", ...) do
|
||||
local x = select(i, ...)
|
||||
|
||||
if type(x) == "number" and config.float_precision then
|
||||
x = tostring(round(x, config.float_precision))
|
||||
elseif type(x) == "table" then
|
||||
x = vim.inspect(x)
|
||||
else
|
||||
x = tostring(x)
|
||||
end
|
||||
|
||||
t[#t + 1] = x
|
||||
end
|
||||
return table.concat(t, " ")
|
||||
end
|
||||
|
||||
local log_at_level = function(level, level_config, message_maker, ...)
|
||||
-- Return early if we're below the config.level
|
||||
if level < levels[config.level] then
|
||||
return
|
||||
end
|
||||
local msg = message_maker(...)
|
||||
local info = debug.getinfo(config.info_level or 2, "Sl")
|
||||
local src_path = info.source:sub(2)
|
||||
local src_line = info.currentline
|
||||
-- Output to console
|
||||
if config.use_console then
|
||||
local log_to_console = function()
|
||||
local console_string = config.fmt_msg(true, level_config.name, src_path, src_line, msg)
|
||||
|
||||
if config.highlights and level_config.hl then
|
||||
vim.cmd(string.format("echohl %s", level_config.hl))
|
||||
end
|
||||
|
||||
local split_console = vim.split(console_string, "\n")
|
||||
for _, v in ipairs(split_console) do
|
||||
local formatted_msg = string.format("[%s] %s", config.plugin, vim.fn.escape(v, [["\]]))
|
||||
|
||||
local ok = pcall(vim.cmd, string.format([[echom "%s"]], formatted_msg))
|
||||
if not ok then
|
||||
vim.api.nvim_out_write(msg .. "\n")
|
||||
end
|
||||
end
|
||||
|
||||
if config.highlights and level_config.hl then
|
||||
vim.cmd "echohl NONE"
|
||||
end
|
||||
end
|
||||
if config.use_console == "sync" and not vim.in_fast_event() then
|
||||
log_to_console()
|
||||
else
|
||||
vim.schedule(log_to_console)
|
||||
end
|
||||
end
|
||||
|
||||
-- Output to log file
|
||||
if config.use_file then
|
||||
local outfile_parent_path = Path:new(outfile):parent()
|
||||
if not outfile_parent_path:exists() then
|
||||
outfile_parent_path:mkdir { parents = true }
|
||||
end
|
||||
local fp = assert(io.open(outfile, "a"))
|
||||
local str = config.fmt_msg(false, level_config.name, src_path, src_line, msg)
|
||||
fp:write(str)
|
||||
fp:close()
|
||||
end
|
||||
|
||||
-- Output to quickfix
|
||||
if config.use_quickfix then
|
||||
local nameupper = level_config.name:upper()
|
||||
local formatted_msg = string.format("[%s] %s", nameupper, msg)
|
||||
local qf_entry = {
|
||||
-- remove the @ getinfo adds to the file path
|
||||
filename = info.source:sub(2),
|
||||
lnum = info.currentline,
|
||||
col = 1,
|
||||
text = formatted_msg,
|
||||
}
|
||||
vim.fn.setqflist({ qf_entry }, "a")
|
||||
end
|
||||
end
|
||||
|
||||
for i, x in ipairs(config.modes) do
|
||||
-- log.info("these", "are", "separated")
|
||||
obj[x.name] = function(...)
|
||||
return log_at_level(i, x, make_string, ...)
|
||||
end
|
||||
|
||||
-- log.fmt_info("These are %s strings", "formatted")
|
||||
obj[("fmt_%s"):format(x.name)] = function(...)
|
||||
return log_at_level(i, x, function(...)
|
||||
local passed = { ... }
|
||||
local fmt = table.remove(passed, 1)
|
||||
local inspected = {}
|
||||
for _, v in ipairs(passed) do
|
||||
table.insert(inspected, vim.inspect(v))
|
||||
end
|
||||
return string.format(fmt, unpack(inspected))
|
||||
end, ...)
|
||||
end
|
||||
|
||||
-- log.lazy_info(expensive_to_calculate)
|
||||
obj[("lazy_%s"):format(x.name)] = function()
|
||||
return log_at_level(i, x, function(f)
|
||||
return f()
|
||||
end)
|
||||
end
|
||||
|
||||
-- log.file_info("do not print")
|
||||
obj[("file_%s"):format(x.name)] = function(vals, override)
|
||||
local original_console = config.use_console
|
||||
config.use_console = false
|
||||
config.info_level = override.info_level
|
||||
log_at_level(i, x, make_string, unpack(vals))
|
||||
config.use_console = original_console
|
||||
config.info_level = nil
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
end
|
||||
|
||||
log.new(default_config, true)
|
||||
-- }}}
|
||||
|
||||
return log
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
local vim = vim
|
||||
|
||||
local M = {}
|
||||
|
||||
M._original_functions = {}
|
||||
|
||||
--- Override an lsp method default callback
|
||||
--- @param method string
|
||||
--- @param new_function function
|
||||
function M.override(method, new_function)
|
||||
if M._original_functions[method] == nil then
|
||||
M._original_functions[method] = vim.lsp.callbacks[method]
|
||||
end
|
||||
|
||||
vim.lsp.callbacks[method] = new_function
|
||||
end
|
||||
|
||||
--- Get the original method callback
|
||||
--- useful if you only want to override in some circumstances
|
||||
---
|
||||
--- @param method string
|
||||
function M.get_original_function(method)
|
||||
if M._original_functions[method] == nil then
|
||||
M._original_functions[method] = vim.lsp.callbacks[method]
|
||||
end
|
||||
|
||||
return M._original_functions[method]
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1 @@
|
|||
error "neorocks is no longer supported. Please use packer.nvim or other project for neorocks usage."
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
local get_lua_version = function()
|
||||
if jit then
|
||||
return {
|
||||
lua = string.gsub(_VERSION, "Lua ", ""),
|
||||
jit = not not string.find(jit.version, "LuaJIT"),
|
||||
version = string.gsub(jit.version, "LuaJIT ", ""),
|
||||
}
|
||||
end
|
||||
|
||||
error("NEOROCKS: Unsupported Lua Versions", _VERSION)
|
||||
end
|
||||
|
||||
return {
|
||||
-- Is run in `--headless` mode.
|
||||
is_headless = (#vim.api.nvim_list_uis() == 0),
|
||||
|
||||
lua_jit = get_lua_version(),
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
---@brief [[
|
||||
---Operators that are functions.
|
||||
---This is useful when you want to pass operators to higher order functions.
|
||||
---Lua has no currying so we have to make a function for each operator.
|
||||
---@brief ]]
|
||||
|
||||
return {
|
||||
----------------------------------------------------------------------------
|
||||
-- Comparison operators
|
||||
----------------------------------------------------------------------------
|
||||
lt = function(a, b)
|
||||
return a < b
|
||||
end,
|
||||
le = function(a, b)
|
||||
return a <= b
|
||||
end,
|
||||
eq = function(a, b)
|
||||
return a == b
|
||||
end,
|
||||
ne = function(a, b)
|
||||
return a ~= b
|
||||
end,
|
||||
ge = function(a, b)
|
||||
return a >= b
|
||||
end,
|
||||
gt = function(a, b)
|
||||
return a > b
|
||||
end,
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- Arithmetic operators
|
||||
----------------------------------------------------------------------------
|
||||
add = function(a, b)
|
||||
return a + b
|
||||
end,
|
||||
div = function(a, b)
|
||||
return a / b
|
||||
end,
|
||||
floordiv = function(a, b)
|
||||
return math.floor(a / b)
|
||||
end,
|
||||
intdiv = function(a, b)
|
||||
local q = a / b
|
||||
if a >= 0 then
|
||||
return math.floor(q)
|
||||
else
|
||||
return math.ceil(q)
|
||||
end
|
||||
end,
|
||||
mod = function(a, b)
|
||||
return a % b
|
||||
end,
|
||||
mul = function(a, b)
|
||||
return a * b
|
||||
end,
|
||||
neq = function(a)
|
||||
return -a
|
||||
end,
|
||||
unm = function(a)
|
||||
return -a
|
||||
end, -- an alias
|
||||
pow = function(a, b)
|
||||
return a ^ b
|
||||
end,
|
||||
sub = function(a, b)
|
||||
return a - b
|
||||
end,
|
||||
truediv = function(a, b)
|
||||
return a / b
|
||||
end,
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- String operators
|
||||
----------------------------------------------------------------------------
|
||||
concat = function(a, b)
|
||||
return a .. b
|
||||
end,
|
||||
len = function(a)
|
||||
return #a
|
||||
end,
|
||||
length = function(a)
|
||||
return #a
|
||||
end, -- an alias
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- Logical operators
|
||||
----------------------------------------------------------------------------
|
||||
land = function(a, b)
|
||||
return a and b
|
||||
end,
|
||||
lor = function(a, b)
|
||||
return a or b
|
||||
end,
|
||||
lnot = function(a)
|
||||
return not a
|
||||
end,
|
||||
truth = function(a)
|
||||
return not not a
|
||||
end,
|
||||
}
|
||||
947
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/path.lua
Normal file
947
.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/path.lua
Normal file
|
|
@ -0,0 +1,947 @@
|
|||
--- Path.lua
|
||||
---
|
||||
--- Goal: Create objects that are extremely similar to Python's `Path` Objects.
|
||||
--- Reference: https://docs.python.org/3/library/pathlib.html
|
||||
|
||||
local bit = require "plenary.bit"
|
||||
local uv = vim.loop
|
||||
|
||||
local F = require "plenary.functional"
|
||||
|
||||
local S_IF = {
|
||||
-- S_IFDIR = 0o040000 # directory
|
||||
DIR = 0x4000,
|
||||
-- S_IFREG = 0o100000 # regular file
|
||||
REG = 0x8000,
|
||||
}
|
||||
|
||||
local path = {}
|
||||
path.home = vim.loop.os_homedir()
|
||||
|
||||
path.sep = (function()
|
||||
if jit then
|
||||
local os = string.lower(jit.os)
|
||||
if os ~= "windows" then
|
||||
return "/"
|
||||
else
|
||||
return "\\"
|
||||
end
|
||||
else
|
||||
return package.config:sub(1, 1)
|
||||
end
|
||||
end)()
|
||||
|
||||
path.root = (function()
|
||||
if path.sep == "/" then
|
||||
return function()
|
||||
return "/"
|
||||
end
|
||||
else
|
||||
return function(base)
|
||||
base = base or vim.loop.cwd()
|
||||
return base:sub(1, 1) .. ":\\"
|
||||
end
|
||||
end
|
||||
end)()
|
||||
|
||||
path.S_IF = S_IF
|
||||
|
||||
local band = function(reg, value)
|
||||
return bit.band(reg, value) == reg
|
||||
end
|
||||
|
||||
local concat_paths = function(...)
|
||||
return table.concat({ ... }, path.sep)
|
||||
end
|
||||
|
||||
local function is_root(pathname)
|
||||
if path.sep == "\\" then
|
||||
return string.match(pathname, "^[A-Z]:\\?$")
|
||||
end
|
||||
return pathname == "/"
|
||||
end
|
||||
|
||||
local _split_by_separator = (function()
|
||||
local formatted = string.format("([^%s]+)", path.sep)
|
||||
return function(filepath)
|
||||
local t = {}
|
||||
for str in string.gmatch(filepath, formatted) do
|
||||
table.insert(t, str)
|
||||
end
|
||||
return t
|
||||
end
|
||||
end)()
|
||||
|
||||
local is_uri = function(filename)
|
||||
return string.match(filename, "^%a[%w+-.]*://") ~= nil
|
||||
end
|
||||
|
||||
local is_absolute = function(filename, sep)
|
||||
if sep == "\\" then
|
||||
return string.match(filename, "^[%a]:[\\/].*$") ~= nil
|
||||
end
|
||||
return string.sub(filename, 1, 1) == sep
|
||||
end
|
||||
|
||||
local function _normalize_path(filename, cwd)
|
||||
if is_uri(filename) then
|
||||
return filename
|
||||
end
|
||||
|
||||
-- handles redundant `./` in the middle
|
||||
local redundant = path.sep .. "%." .. path.sep
|
||||
if filename:match(redundant) then
|
||||
filename = filename:gsub(redundant, path.sep)
|
||||
end
|
||||
|
||||
local out_file = filename
|
||||
|
||||
local has = string.find(filename, path.sep .. "..", 1, true) or string.find(filename, ".." .. path.sep, 1, true)
|
||||
|
||||
if has then
|
||||
local is_abs = is_absolute(filename, path.sep)
|
||||
local split_without_disk_name = function(filename_local)
|
||||
local parts = _split_by_separator(filename_local)
|
||||
-- Remove disk name part on Windows
|
||||
if path.sep == "\\" and is_abs then
|
||||
table.remove(parts, 1)
|
||||
end
|
||||
return parts
|
||||
end
|
||||
|
||||
local parts = split_without_disk_name(filename)
|
||||
local idx = 1
|
||||
local initial_up_count = 0
|
||||
|
||||
repeat
|
||||
if parts[idx] == ".." then
|
||||
if idx == 1 then
|
||||
initial_up_count = initial_up_count + 1
|
||||
end
|
||||
table.remove(parts, idx)
|
||||
table.remove(parts, idx - 1)
|
||||
if idx > 1 then
|
||||
idx = idx - 2
|
||||
else
|
||||
idx = idx - 1
|
||||
end
|
||||
end
|
||||
idx = idx + 1
|
||||
until idx > #parts
|
||||
|
||||
local prefix = ""
|
||||
if is_abs or #split_without_disk_name(cwd) == initial_up_count then
|
||||
prefix = path.root(filename)
|
||||
end
|
||||
|
||||
out_file = prefix .. table.concat(parts, path.sep)
|
||||
end
|
||||
|
||||
return out_file
|
||||
end
|
||||
|
||||
local clean = function(pathname)
|
||||
if is_uri(pathname) then
|
||||
return pathname
|
||||
end
|
||||
|
||||
-- Remove double path seps, it's annoying
|
||||
pathname = pathname:gsub(path.sep .. path.sep, path.sep)
|
||||
|
||||
-- Remove trailing path sep if not root
|
||||
if not is_root(pathname) and pathname:sub(-1) == path.sep then
|
||||
return pathname:sub(1, -2)
|
||||
end
|
||||
return pathname
|
||||
end
|
||||
|
||||
-- S_IFCHR = 0o020000 # character device
|
||||
-- S_IFBLK = 0o060000 # block device
|
||||
-- S_IFIFO = 0o010000 # fifo (named pipe)
|
||||
-- S_IFLNK = 0o120000 # symbolic link
|
||||
-- S_IFSOCK = 0o140000 # socket file
|
||||
|
||||
---@class Path
|
||||
local Path = {
|
||||
path = path,
|
||||
}
|
||||
|
||||
local check_self = function(self)
|
||||
if type(self) == "string" then
|
||||
return Path:new(self)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
Path.__index = function(t, k)
|
||||
local raw = rawget(Path, k)
|
||||
if raw then
|
||||
return raw
|
||||
end
|
||||
|
||||
if k == "_cwd" then
|
||||
local cwd = uv.fs_realpath "."
|
||||
t._cwd = cwd
|
||||
return cwd
|
||||
end
|
||||
|
||||
if k == "_absolute" then
|
||||
local absolute = uv.fs_realpath(t.filename)
|
||||
t._absolute = absolute
|
||||
return absolute
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: Could use this to not have to call new... not sure
|
||||
-- Path.__call = Path:new
|
||||
|
||||
Path.__div = function(self, other)
|
||||
assert(Path.is_path(self))
|
||||
assert(Path.is_path(other) or type(other) == "string")
|
||||
|
||||
return self:joinpath(other)
|
||||
end
|
||||
|
||||
Path.__tostring = function(self)
|
||||
return clean(self.filename)
|
||||
end
|
||||
|
||||
-- TODO: See where we concat the table, and maybe we could make this work.
|
||||
Path.__concat = function(self, other)
|
||||
return self.filename .. other
|
||||
end
|
||||
|
||||
Path.is_path = function(a)
|
||||
return getmetatable(a) == Path
|
||||
end
|
||||
|
||||
function Path:new(...)
|
||||
local args = { ... }
|
||||
|
||||
if type(self) == "string" then
|
||||
table.insert(args, 1, self)
|
||||
self = Path -- luacheck: ignore
|
||||
end
|
||||
|
||||
local path_input
|
||||
if #args == 1 then
|
||||
path_input = args[1]
|
||||
else
|
||||
path_input = args
|
||||
end
|
||||
|
||||
-- If we already have a Path, it's fine.
|
||||
-- Just return it
|
||||
if Path.is_path(path_input) then
|
||||
return path_input
|
||||
end
|
||||
|
||||
-- TODO: Should probably remove and dumb stuff like double seps, periods in the middle, etc.
|
||||
local sep = path.sep
|
||||
if type(path_input) == "table" then
|
||||
sep = path_input.sep or path.sep
|
||||
path_input.sep = nil
|
||||
end
|
||||
|
||||
local path_string
|
||||
if type(path_input) == "table" then
|
||||
-- TODO: It's possible this could be done more elegantly with __concat
|
||||
-- But I'm unsure of what we'd do to make that happen
|
||||
local path_objs = {}
|
||||
for _, v in ipairs(path_input) do
|
||||
if Path.is_path(v) then
|
||||
table.insert(path_objs, v.filename)
|
||||
else
|
||||
assert(type(v) == "string")
|
||||
table.insert(path_objs, v)
|
||||
end
|
||||
end
|
||||
|
||||
path_string = table.concat(path_objs, sep)
|
||||
else
|
||||
assert(type(path_input) == "string", vim.inspect(path_input))
|
||||
path_string = path_input
|
||||
end
|
||||
|
||||
local obj = {
|
||||
filename = path_string,
|
||||
|
||||
_sep = sep,
|
||||
}
|
||||
|
||||
setmetatable(obj, Path)
|
||||
|
||||
return obj
|
||||
end
|
||||
|
||||
function Path:_fs_filename()
|
||||
return self:absolute() or self.filename
|
||||
end
|
||||
|
||||
function Path:_stat()
|
||||
return uv.fs_stat(self:_fs_filename()) or {}
|
||||
-- local stat = uv.fs_stat(self:absolute())
|
||||
-- if not self._absolute then return {} end
|
||||
|
||||
-- if not self._stat_result then
|
||||
-- self._stat_result =
|
||||
-- end
|
||||
|
||||
-- return self._stat_result
|
||||
end
|
||||
|
||||
function Path:_st_mode()
|
||||
return self:_stat().mode or 0
|
||||
end
|
||||
|
||||
function Path:joinpath(...)
|
||||
return Path:new(self.filename, ...)
|
||||
end
|
||||
|
||||
function Path:absolute()
|
||||
if self:is_absolute() then
|
||||
return _normalize_path(self.filename, self._cwd)
|
||||
else
|
||||
return _normalize_path(self._absolute or table.concat({ self._cwd, self.filename }, self._sep), self._cwd)
|
||||
end
|
||||
end
|
||||
|
||||
function Path:exists()
|
||||
return not vim.tbl_isempty(self:_stat())
|
||||
end
|
||||
|
||||
function Path:expand()
|
||||
if is_uri(self.filename) then
|
||||
return self.filename
|
||||
end
|
||||
|
||||
-- TODO support windows
|
||||
local expanded
|
||||
if string.find(self.filename, "~") then
|
||||
expanded = string.gsub(self.filename, "^~", vim.loop.os_homedir())
|
||||
elseif string.find(self.filename, "^%.") then
|
||||
expanded = vim.loop.fs_realpath(self.filename)
|
||||
if expanded == nil then
|
||||
expanded = vim.fn.fnamemodify(self.filename, ":p")
|
||||
end
|
||||
elseif string.find(self.filename, "%$") then
|
||||
local rep = string.match(self.filename, "([^%$][^/]*)")
|
||||
local val = os.getenv(rep)
|
||||
if val then
|
||||
expanded = string.gsub(string.gsub(self.filename, rep, val), "%$", "")
|
||||
else
|
||||
expanded = nil
|
||||
end
|
||||
else
|
||||
expanded = self.filename
|
||||
end
|
||||
return expanded and expanded or error "Path not valid"
|
||||
end
|
||||
|
||||
function Path:make_relative(cwd)
|
||||
if is_uri(self.filename) then
|
||||
return self.filename
|
||||
end
|
||||
|
||||
self.filename = clean(self.filename)
|
||||
cwd = clean(F.if_nil(cwd, self._cwd, cwd))
|
||||
if self.filename == cwd then
|
||||
self.filename = "."
|
||||
else
|
||||
if cwd:sub(#cwd, #cwd) ~= path.sep then
|
||||
cwd = cwd .. path.sep
|
||||
end
|
||||
|
||||
if self.filename:sub(1, #cwd) == cwd then
|
||||
self.filename = self.filename:sub(#cwd + 1, -1)
|
||||
end
|
||||
end
|
||||
|
||||
return self.filename
|
||||
end
|
||||
|
||||
function Path:normalize(cwd)
|
||||
if is_uri(self.filename) then
|
||||
return self.filename
|
||||
end
|
||||
|
||||
self:make_relative(cwd)
|
||||
|
||||
-- Substitute home directory w/ "~"
|
||||
-- string.gsub is not useful here because usernames with dashes at the end
|
||||
-- will be seen as a regexp pattern rather than a raw string
|
||||
local home = path.home
|
||||
if string.sub(path.home, -1) ~= path.sep then
|
||||
home = home .. path.sep
|
||||
end
|
||||
local start, finish = string.find(self.filename, home, 1, true)
|
||||
if start == 1 then
|
||||
self.filename = "~" .. path.sep .. string.sub(self.filename, (finish + 1), -1)
|
||||
end
|
||||
|
||||
return _normalize_path(clean(self.filename), self._cwd)
|
||||
end
|
||||
|
||||
local function shorten_len(filename, len, exclude)
|
||||
len = len or 1
|
||||
exclude = exclude or { -1 }
|
||||
local exc = {}
|
||||
|
||||
-- get parts in a table
|
||||
local parts = {}
|
||||
local empty_pos = {}
|
||||
for m in (filename .. path.sep):gmatch("(.-)" .. path.sep) do
|
||||
if m ~= "" then
|
||||
parts[#parts + 1] = m
|
||||
else
|
||||
table.insert(empty_pos, #parts + 1)
|
||||
end
|
||||
end
|
||||
|
||||
for _, v in pairs(exclude) do
|
||||
if v < 0 then
|
||||
exc[v + #parts + 1] = true
|
||||
else
|
||||
exc[v] = true
|
||||
end
|
||||
end
|
||||
|
||||
local final_path_components = {}
|
||||
local count = 1
|
||||
for _, match in ipairs(parts) do
|
||||
if not exc[count] and #match > len then
|
||||
table.insert(final_path_components, string.sub(match, 1, len))
|
||||
else
|
||||
table.insert(final_path_components, match)
|
||||
end
|
||||
table.insert(final_path_components, path.sep)
|
||||
count = count + 1
|
||||
end
|
||||
|
||||
local l = #final_path_components -- so that we don't need to keep calculating length
|
||||
table.remove(final_path_components, l) -- remove final slash
|
||||
|
||||
-- add back empty positions
|
||||
for i = #empty_pos, 1, -1 do
|
||||
table.insert(final_path_components, empty_pos[i], path.sep)
|
||||
end
|
||||
|
||||
return table.concat(final_path_components)
|
||||
end
|
||||
|
||||
local shorten = (function()
|
||||
local fallback = function(filename)
|
||||
return shorten_len(filename, 1)
|
||||
end
|
||||
|
||||
if jit and path.sep ~= "\\" then
|
||||
local ffi = require "ffi"
|
||||
ffi.cdef [[
|
||||
typedef unsigned char char_u;
|
||||
void shorten_dir(char_u *str);
|
||||
]]
|
||||
local ffi_func = function(filename)
|
||||
if not filename or is_uri(filename) then
|
||||
return filename
|
||||
end
|
||||
|
||||
local c_str = ffi.new("char[?]", #filename + 1)
|
||||
ffi.copy(c_str, filename)
|
||||
ffi.C.shorten_dir(c_str)
|
||||
return ffi.string(c_str)
|
||||
end
|
||||
local ok = pcall(ffi_func, "/tmp/path/file.lua")
|
||||
if ok then
|
||||
return ffi_func
|
||||
else
|
||||
return fallback
|
||||
end
|
||||
end
|
||||
return fallback
|
||||
end)()
|
||||
|
||||
function Path:shorten(len, exclude)
|
||||
assert(len ~= 0, "len must be at least 1")
|
||||
if (len and len > 1) or exclude ~= nil then
|
||||
return shorten_len(self.filename, len, exclude)
|
||||
end
|
||||
return shorten(self.filename)
|
||||
end
|
||||
|
||||
function Path:mkdir(opts)
|
||||
opts = opts or {}
|
||||
|
||||
local mode = opts.mode or 448 -- 0700 -> decimal
|
||||
local parents = F.if_nil(opts.parents, false, opts.parents)
|
||||
local exists_ok = F.if_nil(opts.exists_ok, true, opts.exists_ok)
|
||||
|
||||
local exists = self:exists()
|
||||
if not exists_ok and exists then
|
||||
error("FileExistsError:" .. self:absolute())
|
||||
end
|
||||
|
||||
-- fs_mkdir returns nil if folder exists
|
||||
if not uv.fs_mkdir(self:_fs_filename(), mode) and not exists then
|
||||
if parents then
|
||||
local dirs = self:_split()
|
||||
local processed = ""
|
||||
for _, dir in ipairs(dirs) do
|
||||
if dir ~= "" then
|
||||
local joined = concat_paths(processed, dir)
|
||||
if processed == "" and self._sep == "\\" then
|
||||
joined = dir
|
||||
end
|
||||
local stat = uv.fs_stat(joined) or {}
|
||||
local file_mode = stat.mode or 0
|
||||
if band(S_IF.REG, file_mode) then
|
||||
error(string.format("%s is a regular file so we can't mkdir it", joined))
|
||||
elseif band(S_IF.DIR, file_mode) then
|
||||
processed = joined
|
||||
else
|
||||
if uv.fs_mkdir(joined, mode) then
|
||||
processed = joined
|
||||
else
|
||||
error("We couldn't mkdir: " .. joined)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
error "FileNotFoundError"
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function Path:rmdir()
|
||||
if not self:exists() then
|
||||
return
|
||||
end
|
||||
|
||||
uv.fs_rmdir(self:absolute())
|
||||
end
|
||||
|
||||
function Path:rename(opts)
|
||||
opts = opts or {}
|
||||
if not opts.new_name or opts.new_name == "" then
|
||||
error "Please provide the new name!"
|
||||
end
|
||||
|
||||
-- handles `.`, `..`, `./`, and `../`
|
||||
if opts.new_name:match "^%.%.?/?\\?.+" then
|
||||
opts.new_name = {
|
||||
uv.fs_realpath(opts.new_name:sub(1, 3)),
|
||||
opts.new_name:sub(4, #opts.new_name),
|
||||
}
|
||||
end
|
||||
|
||||
local new_path = Path:new(opts.new_name)
|
||||
|
||||
if new_path:exists() then
|
||||
error "File or directory already exists!"
|
||||
end
|
||||
|
||||
local status = uv.fs_rename(self:absolute(), new_path:absolute())
|
||||
self.filename = new_path.filename
|
||||
|
||||
return status
|
||||
end
|
||||
|
||||
--- Copy files or folders with defaults akin to GNU's `cp`.
|
||||
---@param opts table: options to pass to toggling registered actions
|
||||
---@field destination string|Path: target file path to copy to
|
||||
---@field recursive bool: whether to copy folders recursively (default: false)
|
||||
---@field override bool: whether to override files (default: true)
|
||||
---@field interactive bool: confirm if copy would override; precedes `override` (default: false)
|
||||
---@field respect_gitignore bool: skip folders ignored by all detected `gitignore`s (default: false)
|
||||
---@field hidden bool: whether to add hidden files in recursively copying folders (default: true)
|
||||
---@field parents bool: whether to create possibly non-existing parent dirs of `opts.destination` (default: false)
|
||||
---@field exists_ok bool: whether ok if `opts.destination` exists, if so folders are merged (default: true)
|
||||
---@return table {[Path of destination]: bool} indicating success of copy; nested tables constitute sub dirs
|
||||
function Path:copy(opts)
|
||||
opts = opts or {}
|
||||
opts.recursive = F.if_nil(opts.recursive, false, opts.recursive)
|
||||
opts.override = F.if_nil(opts.override, true, opts.override)
|
||||
|
||||
local dest = opts.destination
|
||||
-- handles `.`, `..`, `./`, and `../`
|
||||
if not Path.is_path(dest) then
|
||||
if type(dest) == "string" and dest:match "^%.%.?/?\\?.+" then
|
||||
dest = {
|
||||
uv.fs_realpath(dest:sub(1, 3)),
|
||||
dest:sub(4, #dest),
|
||||
}
|
||||
end
|
||||
dest = Path:new(dest)
|
||||
end
|
||||
-- success is true in case file is copied, false otherwise
|
||||
local success = {}
|
||||
if not self:is_dir() then
|
||||
if opts.interactive and dest:exists() then
|
||||
vim.ui.select(
|
||||
{ "Yes", "No" },
|
||||
{ prompt = string.format("Overwrite existing %s?", dest:absolute()) },
|
||||
function(_, idx)
|
||||
success[dest] = uv.fs_copyfile(self:absolute(), dest:absolute(), { excl = idx ~= 1 }) or false
|
||||
end
|
||||
)
|
||||
else
|
||||
-- nil: not overriden if `override = false`
|
||||
success[dest] = uv.fs_copyfile(self:absolute(), dest:absolute(), { excl = not opts.override }) or false
|
||||
end
|
||||
return success
|
||||
end
|
||||
-- dir
|
||||
if opts.recursive then
|
||||
dest:mkdir {
|
||||
parents = F.if_nil(opts.parents, false, opts.parents),
|
||||
exists_ok = F.if_nil(opts.exists_ok, true, opts.exists_ok),
|
||||
}
|
||||
local scan = require "plenary.scandir"
|
||||
local data = scan.scan_dir(self.filename, {
|
||||
respect_gitignore = F.if_nil(opts.respect_gitignore, false, opts.respect_gitignore),
|
||||
hidden = F.if_nil(opts.hidden, true, opts.hidden),
|
||||
depth = 1,
|
||||
add_dirs = true,
|
||||
})
|
||||
for _, entry in ipairs(data) do
|
||||
local entry_path = Path:new(entry)
|
||||
local suffix = table.remove(entry_path:_split())
|
||||
local new_dest = dest:joinpath(suffix)
|
||||
-- clear destination as it might be Path table otherwise failing w/ extend
|
||||
opts.destination = nil
|
||||
local new_opts = vim.tbl_deep_extend("force", opts, { destination = new_dest })
|
||||
-- nil: not overriden if `override = false`
|
||||
success[new_dest] = entry_path:copy(new_opts) or false
|
||||
end
|
||||
return success
|
||||
else
|
||||
error(string.format("Warning: %s was not copied as `recursive=false`", self:absolute()))
|
||||
end
|
||||
end
|
||||
|
||||
function Path:touch(opts)
|
||||
opts = opts or {}
|
||||
|
||||
local mode = opts.mode or 420
|
||||
local parents = F.if_nil(opts.parents, false, opts.parents)
|
||||
|
||||
if self:exists() then
|
||||
local new_time = os.time()
|
||||
uv.fs_utime(self:_fs_filename(), new_time, new_time)
|
||||
return
|
||||
end
|
||||
|
||||
if parents then
|
||||
Path:new(self:parent()):mkdir { parents = true }
|
||||
end
|
||||
|
||||
local fd = uv.fs_open(self:_fs_filename(), "w", mode)
|
||||
if not fd then
|
||||
error("Could not create file: " .. self:_fs_filename())
|
||||
end
|
||||
uv.fs_close(fd)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function Path:rm(opts)
|
||||
opts = opts or {}
|
||||
|
||||
local recursive = F.if_nil(opts.recursive, false, opts.recursive)
|
||||
if recursive then
|
||||
local scan = require "plenary.scandir"
|
||||
local abs = self:absolute()
|
||||
|
||||
-- first unlink all files
|
||||
scan.scan_dir(abs, {
|
||||
hidden = true,
|
||||
on_insert = function(file)
|
||||
uv.fs_unlink(file)
|
||||
end,
|
||||
})
|
||||
|
||||
local dirs = scan.scan_dir(abs, { add_dirs = true, hidden = true })
|
||||
-- iterate backwards to clean up remaining dirs
|
||||
for i = #dirs, 1, -1 do
|
||||
uv.fs_rmdir(dirs[i])
|
||||
end
|
||||
|
||||
-- now only abs is missing
|
||||
uv.fs_rmdir(abs)
|
||||
else
|
||||
uv.fs_unlink(self:absolute())
|
||||
end
|
||||
end
|
||||
|
||||
-- Path:is_* {{{
|
||||
function Path:is_dir()
|
||||
-- TODO: I wonder when this would be better, if ever.
|
||||
-- return self:_stat().type == 'directory'
|
||||
|
||||
return band(S_IF.DIR, self:_st_mode())
|
||||
end
|
||||
|
||||
function Path:is_absolute()
|
||||
return is_absolute(self.filename, self._sep)
|
||||
end
|
||||
-- }}}
|
||||
|
||||
function Path:_split()
|
||||
return vim.split(self:absolute(), self._sep)
|
||||
end
|
||||
|
||||
local _get_parent = (function()
|
||||
local formatted = string.format("^(.+)%s[^%s]+", path.sep, path.sep)
|
||||
return function(abs_path)
|
||||
local parent = abs_path:match(formatted)
|
||||
if parent ~= nil and not parent:find(path.sep) then
|
||||
return parent .. path.sep
|
||||
end
|
||||
return parent
|
||||
end
|
||||
end)()
|
||||
|
||||
function Path:parent()
|
||||
return Path:new(_get_parent(self:absolute()) or path.root(self:absolute()))
|
||||
end
|
||||
|
||||
function Path:parents()
|
||||
local results = {}
|
||||
local cur = self:absolute()
|
||||
repeat
|
||||
cur = _get_parent(cur)
|
||||
table.insert(results, cur)
|
||||
until not cur
|
||||
table.insert(results, path.root(self:absolute()))
|
||||
return results
|
||||
end
|
||||
|
||||
function Path:is_file()
|
||||
return self:_stat().type == "file" and true or nil
|
||||
end
|
||||
|
||||
-- TODO:
|
||||
-- Maybe I can use libuv for this?
|
||||
function Path:open() end
|
||||
|
||||
function Path:close() end
|
||||
|
||||
function Path:write(txt, flag, mode)
|
||||
assert(flag, [[Path:write_text requires a flag! For example: 'w' or 'a']])
|
||||
|
||||
mode = mode or 438
|
||||
|
||||
local fd = assert(uv.fs_open(self:_fs_filename(), flag, mode))
|
||||
assert(uv.fs_write(fd, txt, -1))
|
||||
assert(uv.fs_close(fd))
|
||||
end
|
||||
|
||||
-- TODO: Asyncify this and use vim.wait in the meantime.
|
||||
-- This will allow other events to happen while we're waiting!
|
||||
function Path:_read()
|
||||
self = check_self(self)
|
||||
|
||||
local fd = assert(uv.fs_open(self:_fs_filename(), "r", 438)) -- for some reason test won't pass with absolute
|
||||
local stat = assert(uv.fs_fstat(fd))
|
||||
local data = assert(uv.fs_read(fd, stat.size, 0))
|
||||
assert(uv.fs_close(fd))
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
function Path:_read_async(callback)
|
||||
vim.loop.fs_open(self.filename, "r", 438, function(err_open, fd)
|
||||
if err_open then
|
||||
print("We tried to open this file but couldn't. We failed with following error message: " .. err_open)
|
||||
return
|
||||
end
|
||||
vim.loop.fs_fstat(fd, function(err_fstat, stat)
|
||||
assert(not err_fstat, err_fstat)
|
||||
if stat.type ~= "file" then
|
||||
return callback ""
|
||||
end
|
||||
vim.loop.fs_read(fd, stat.size, 0, function(err_read, data)
|
||||
assert(not err_read, err_read)
|
||||
vim.loop.fs_close(fd, function(err_close)
|
||||
assert(not err_close, err_close)
|
||||
return callback(data)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function Path:read(callback)
|
||||
if callback then
|
||||
return self:_read_async(callback)
|
||||
end
|
||||
return self:_read()
|
||||
end
|
||||
|
||||
function Path:head(lines)
|
||||
lines = lines or 10
|
||||
self = check_self(self)
|
||||
local chunk_size = 256
|
||||
|
||||
local fd = uv.fs_open(self:_fs_filename(), "r", 438)
|
||||
if not fd then
|
||||
return
|
||||
end
|
||||
local stat = assert(uv.fs_fstat(fd))
|
||||
if stat.type ~= "file" then
|
||||
uv.fs_close(fd)
|
||||
return nil
|
||||
end
|
||||
|
||||
local data = ""
|
||||
local index, count = 0, 0
|
||||
while count < lines and index < stat.size do
|
||||
local read_chunk = assert(uv.fs_read(fd, chunk_size, index))
|
||||
|
||||
local i = 0
|
||||
for char in read_chunk:gmatch "." do
|
||||
if char == "\n" then
|
||||
count = count + 1
|
||||
if count >= lines then
|
||||
break
|
||||
end
|
||||
end
|
||||
index = index + 1
|
||||
i = i + 1
|
||||
end
|
||||
data = data .. read_chunk:sub(1, i)
|
||||
end
|
||||
assert(uv.fs_close(fd))
|
||||
|
||||
-- Remove potential newline at end of file
|
||||
if data:sub(-1) == "\n" then
|
||||
data = data:sub(1, -2)
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
function Path:tail(lines)
|
||||
lines = lines or 10
|
||||
self = check_self(self)
|
||||
local chunk_size = 256
|
||||
|
||||
local fd = uv.fs_open(self:_fs_filename(), "r", 438)
|
||||
if not fd then
|
||||
return
|
||||
end
|
||||
local stat = assert(uv.fs_fstat(fd))
|
||||
if stat.type ~= "file" then
|
||||
uv.fs_close(fd)
|
||||
return nil
|
||||
end
|
||||
|
||||
local data = ""
|
||||
local index, count = stat.size - 1, 0
|
||||
while count < lines and index > 0 do
|
||||
local real_index = index - chunk_size
|
||||
if real_index < 0 then
|
||||
chunk_size = chunk_size + real_index
|
||||
real_index = 0
|
||||
end
|
||||
|
||||
local read_chunk = assert(uv.fs_read(fd, chunk_size, real_index))
|
||||
|
||||
local i = #read_chunk
|
||||
while i > 0 do
|
||||
local char = read_chunk:sub(i, i)
|
||||
if char == "\n" then
|
||||
count = count + 1
|
||||
if count >= lines then
|
||||
break
|
||||
end
|
||||
end
|
||||
index = index - 1
|
||||
i = i - 1
|
||||
end
|
||||
data = read_chunk:sub(i + 1, #read_chunk) .. data
|
||||
end
|
||||
assert(uv.fs_close(fd))
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
function Path:readlines()
|
||||
self = check_self(self)
|
||||
|
||||
local data = self:read()
|
||||
|
||||
data = data:gsub("\r", "")
|
||||
return vim.split(data, "\n")
|
||||
end
|
||||
|
||||
function Path:iter()
|
||||
local data = self:readlines()
|
||||
local i = 0
|
||||
local n = #data
|
||||
return function()
|
||||
i = i + 1
|
||||
if i <= n then
|
||||
return data[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Path:readbyterange(offset, length)
|
||||
self = check_self(self)
|
||||
|
||||
local fd = uv.fs_open(self:_fs_filename(), "r", 438)
|
||||
if not fd then
|
||||
return
|
||||
end
|
||||
local stat = assert(uv.fs_fstat(fd))
|
||||
if stat.type ~= "file" then
|
||||
uv.fs_close(fd)
|
||||
return nil
|
||||
end
|
||||
|
||||
if offset < 0 then
|
||||
offset = stat.size + offset
|
||||
-- Windows fails if offset is < 0 even though offset is defined as signed
|
||||
-- http://docs.libuv.org/en/v1.x/fs.html#c.uv_fs_read
|
||||
if offset < 0 then
|
||||
offset = 0
|
||||
end
|
||||
end
|
||||
|
||||
local data = ""
|
||||
while #data < length do
|
||||
local read_chunk = assert(uv.fs_read(fd, length - #data, offset))
|
||||
if #read_chunk == 0 then
|
||||
break
|
||||
end
|
||||
data = data .. read_chunk
|
||||
offset = offset + #read_chunk
|
||||
end
|
||||
|
||||
assert(uv.fs_close(fd))
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
function Path:find_upwards(filename)
|
||||
local folder = Path:new(self)
|
||||
local root = path.root(folder:absolute())
|
||||
|
||||
while true do
|
||||
local p = folder:joinpath(filename)
|
||||
if p:exists() then
|
||||
return p
|
||||
end
|
||||
if folder:absolute() == root then
|
||||
break
|
||||
end
|
||||
folder = folder:parent()
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
return Path
|
||||
|
|
@ -0,0 +1,490 @@
|
|||
--- popup.lua
|
||||
---
|
||||
--- Wrapper to make the popup api from vim in neovim.
|
||||
--- Hope to get this part merged in at some point in the future.
|
||||
---
|
||||
--- Please make sure to update "POPUP.md" with any changes and/or notes.
|
||||
|
||||
local Border = require "plenary.window.border"
|
||||
local Window = require "plenary.window"
|
||||
local utils = require "plenary.popup.utils"
|
||||
|
||||
local if_nil = vim.F.if_nil
|
||||
|
||||
local popup = {}
|
||||
|
||||
popup._pos_map = {
|
||||
topleft = "NW",
|
||||
topright = "NE",
|
||||
botleft = "SW",
|
||||
botright = "SE",
|
||||
}
|
||||
|
||||
-- Keep track of hidden popups, so we can load them with popup.show()
|
||||
popup._hidden = {}
|
||||
|
||||
-- Keep track of popup borders, so we don't have to pass them between functions
|
||||
popup._borders = {}
|
||||
|
||||
local function dict_default(options, key, default)
|
||||
if options[key] == nil then
|
||||
return default[key]
|
||||
else
|
||||
return options[key]
|
||||
end
|
||||
end
|
||||
|
||||
-- Callbacks to be called later by popup.execute_callback
|
||||
popup._callbacks = {}
|
||||
|
||||
-- Convert the positional {vim_options} to compatible neovim options and add them to {win_opts}
|
||||
-- If an option is not given in {vim_options}, fall back to {default_opts}
|
||||
local function add_position_config(win_opts, vim_options, default_opts)
|
||||
default_opts = default_opts or {}
|
||||
|
||||
local cursor_relative_pos = function(pos_str, dim)
|
||||
assert(string.find(pos_str, "^cursor"), "Invalid value for " .. dim)
|
||||
win_opts.relative = "cursor"
|
||||
local line = 0
|
||||
if (pos_str):match "cursor%+(%d+)" then
|
||||
line = line + tonumber((pos_str):match "cursor%+(%d+)")
|
||||
elseif (pos_str):match "cursor%-(%d+)" then
|
||||
line = line - tonumber((pos_str):match "cursor%-(%d+)")
|
||||
end
|
||||
return line
|
||||
end
|
||||
|
||||
-- Feels like maxheight, minheight, maxwidth, minwidth will all be related
|
||||
--
|
||||
-- maxheight Maximum height of the contents, excluding border and padding.
|
||||
-- minheight Minimum height of the contents, excluding border and padding.
|
||||
-- maxwidth Maximum width of the contents, excluding border, padding and scrollbar.
|
||||
-- minwidth Minimum width of the contents, excluding border, padding and scrollbar.
|
||||
local width = if_nil(vim_options.width, default_opts.width)
|
||||
local height = if_nil(vim_options.height, default_opts.height)
|
||||
win_opts.width = utils.bounded(width, vim_options.minwidth, vim_options.maxwidth)
|
||||
win_opts.height = utils.bounded(height, vim_options.minheight, vim_options.maxheight)
|
||||
|
||||
if vim_options.line and vim_options.line ~= 0 then
|
||||
if type(vim_options.line) == "string" then
|
||||
win_opts.row = cursor_relative_pos(vim_options.line, "row")
|
||||
else
|
||||
win_opts.row = vim_options.line - 1
|
||||
end
|
||||
else
|
||||
win_opts.row = math.floor((vim.o.lines - win_opts.height) / 2)
|
||||
end
|
||||
|
||||
if vim_options.col and vim_options.col ~= 0 then
|
||||
if type(vim_options.col) == "string" then
|
||||
win_opts.col = cursor_relative_pos(vim_options.col, "col")
|
||||
else
|
||||
win_opts.col = vim_options.col - 1
|
||||
end
|
||||
else
|
||||
win_opts.col = math.floor((vim.o.columns - win_opts.width) / 2)
|
||||
end
|
||||
|
||||
-- pos
|
||||
--
|
||||
-- Using "topleft", "topright", "botleft", "botright" defines what corner of the popup "line"
|
||||
-- and "col" are used for. When not set "topleft" behaviour is used.
|
||||
-- Alternatively "center" can be used to position the popup in the center of the Neovim window,
|
||||
-- in which case "line" and "col" are ignored.
|
||||
if vim_options.pos then
|
||||
if vim_options.pos == "center" then
|
||||
vim_options.line = 0
|
||||
vim_options.col = 0
|
||||
win_opts.anchor = "NW"
|
||||
else
|
||||
win_opts.anchor = popup._pos_map[vim_options.pos]
|
||||
end
|
||||
else
|
||||
win_opts.anchor = "NW" -- This is the default, but makes `posinvert` easier to implement
|
||||
end
|
||||
|
||||
-- , fixed When FALSE (the default), and:
|
||||
-- , - "pos" is "botleft" or "topleft", and
|
||||
-- , - "wrap" is off, and
|
||||
-- , - the popup would be truncated at the right edge of
|
||||
-- , the screen, then
|
||||
-- , the popup is moved to the left so as to fit the
|
||||
-- , contents on the screen. Set to TRUE to disable this.
|
||||
end
|
||||
|
||||
function popup.create(what, vim_options)
|
||||
vim_options = vim.deepcopy(vim_options)
|
||||
|
||||
local bufnr
|
||||
if type(what) == "number" then
|
||||
bufnr = what
|
||||
else
|
||||
bufnr = vim.api.nvim_create_buf(false, true)
|
||||
assert(bufnr, "Failed to create buffer")
|
||||
|
||||
vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
|
||||
vim.api.nvim_buf_set_option(bufnr, "modifiable", true)
|
||||
|
||||
-- TODO: Handle list of lines
|
||||
if type(what) == "string" then
|
||||
what = { what }
|
||||
else
|
||||
assert(type(what) == "table", '"what" must be a table')
|
||||
end
|
||||
|
||||
-- padding List with numbers, defining the padding
|
||||
-- above/right/below/left of the popup (similar to CSS).
|
||||
-- An empty list uses a padding of 1 all around. The
|
||||
-- padding goes around the text, inside any border.
|
||||
-- Padding uses the 'wincolor' highlight.
|
||||
-- Example: [1, 2, 1, 3] has 1 line of padding above, 2
|
||||
-- columns on the right, 1 line below and 3 columns on
|
||||
-- the left.
|
||||
if vim_options.padding then
|
||||
local pad_top, pad_right, pad_below, pad_left
|
||||
if vim.tbl_isempty(vim_options.padding) then
|
||||
pad_top = 1
|
||||
pad_right = 1
|
||||
pad_below = 1
|
||||
pad_left = 1
|
||||
else
|
||||
local padding = vim_options.padding
|
||||
pad_top = padding[1] or 0
|
||||
pad_right = padding[2] or 0
|
||||
pad_below = padding[3] or 0
|
||||
pad_left = padding[4] or 0
|
||||
end
|
||||
|
||||
local left_padding = string.rep(" ", pad_left)
|
||||
local right_padding = string.rep(" ", pad_right)
|
||||
for index = 1, #what do
|
||||
what[index] = string.format("%s%s%s", left_padding, what[index], right_padding)
|
||||
end
|
||||
|
||||
for _ = 1, pad_top do
|
||||
table.insert(what, 1, "")
|
||||
end
|
||||
|
||||
for _ = 1, pad_below do
|
||||
table.insert(what, "")
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, what)
|
||||
end
|
||||
|
||||
local option_defaults = {
|
||||
posinvert = true,
|
||||
zindex = 50,
|
||||
}
|
||||
|
||||
vim_options.width = if_nil(vim_options.width, 1)
|
||||
if type(what) == "number" then
|
||||
vim_options.height = vim.api.nvim_buf_line_count(what)
|
||||
else
|
||||
for _, v in ipairs(what) do
|
||||
vim_options.width = math.max(vim_options.width, #v)
|
||||
end
|
||||
vim_options.height = #what
|
||||
end
|
||||
|
||||
local win_opts = {}
|
||||
win_opts.relative = "editor"
|
||||
win_opts.style = "minimal"
|
||||
win_opts.border = "none"
|
||||
|
||||
-- Add positional and sizing config to win_opts
|
||||
add_position_config(win_opts, vim_options, { width = 1, height = 1 })
|
||||
|
||||
-- posinvert, When FALSE the value of "pos" is always used. When
|
||||
-- , TRUE (the default) and the popup does not fit
|
||||
-- , vertically and there is more space on the other side
|
||||
-- , then the popup is placed on the other side of the
|
||||
-- , position indicated by "line".
|
||||
if dict_default(vim_options, "posinvert", option_defaults) then
|
||||
if win_opts.anchor == "NW" or win_opts.anchor == "NE" then
|
||||
if win_opts.row + win_opts.height > vim.o.lines and win_opts.row * 2 > vim.o.lines then
|
||||
-- Don't know why, but this is how vim adjusts it
|
||||
win_opts.row = win_opts.row - win_opts.height - 2
|
||||
end
|
||||
elseif win_opts.anchor == "SW" or win_opts.anchor == "SE" then
|
||||
if win_opts.row - win_opts.height < 0 and win_opts.row * 2 < vim.o.lines then
|
||||
-- Don't know why, but this is how vim adjusts it
|
||||
win_opts.row = win_opts.row + win_opts.height + 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- textprop, When present the popup is positioned next to a text
|
||||
-- , property with this name and will move when the text
|
||||
-- , property moves. Use an empty string to remove. See
|
||||
-- , |popup-textprop-pos|.
|
||||
-- related:
|
||||
-- textpropwin
|
||||
-- textpropid
|
||||
|
||||
-- zindex, Priority for the popup, default 50. Minimum value is
|
||||
-- , 1, maximum value is 32000.
|
||||
local zindex = dict_default(vim_options, "zindex", option_defaults)
|
||||
win_opts.zindex = utils.bounded(zindex, 1, 32000)
|
||||
|
||||
-- noautocmd, undocumented vim default per https://github.com/vim/vim/issues/5737
|
||||
win_opts.noautocmd = if_nil(vim_options.noautocmd, true)
|
||||
|
||||
-- focusable,
|
||||
-- vim popups are not focusable windows
|
||||
win_opts.focusable = if_nil(vim_options.focusable, false)
|
||||
|
||||
local win_id
|
||||
if vim_options.hidden then
|
||||
assert(false, "I have not implemented this yet and don't know how")
|
||||
else
|
||||
win_id = vim.api.nvim_open_win(bufnr, false, win_opts)
|
||||
end
|
||||
|
||||
-- Moved, handled after since we need the window ID
|
||||
if vim_options.moved then
|
||||
if vim_options.moved == "any" then
|
||||
vim.lsp.util.close_preview_autocmd({ "CursorMoved", "CursorMovedI" }, win_id)
|
||||
-- elseif vim_options.moved == "word" then
|
||||
-- TODO: Handle word, WORD, expr, and the range functions... which seem hard?
|
||||
end
|
||||
else
|
||||
local silent = false
|
||||
vim.cmd(
|
||||
string.format(
|
||||
"autocmd BufDelete %s <buffer=%s> ++once ++nested :lua require('plenary.window').try_close(%s, true)",
|
||||
(silent and "<silent>") or "",
|
||||
bufnr,
|
||||
win_id
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
if vim_options.time then
|
||||
local timer = vim.loop.new_timer()
|
||||
timer:start(
|
||||
vim_options.time,
|
||||
0,
|
||||
vim.schedule_wrap(function()
|
||||
Window.try_close(win_id, false)
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
-- Buffer Options
|
||||
if vim_options.cursorline then
|
||||
vim.api.nvim_win_set_option(win_id, "cursorline", true)
|
||||
end
|
||||
|
||||
if vim_options.wrap ~= nil then
|
||||
-- set_option wrap should/will trigger autocmd, see https://github.com/neovim/neovim/pull/13247
|
||||
if vim_options.noautocmd then
|
||||
vim.cmd(string.format("noautocmd lua vim.api.nvim_set_option(%s, wrap, %s)", win_id, vim_options.wrap))
|
||||
else
|
||||
vim.api.nvim_win_set_option(win_id, "wrap", vim_options.wrap)
|
||||
end
|
||||
end
|
||||
|
||||
-- ===== Not Implemented Options =====
|
||||
-- flip: not implemented at the time of writing
|
||||
-- Mouse:
|
||||
-- mousemoved: no idea how to do the things with the mouse, so it's an exercise for the reader.
|
||||
-- drag: mouses are hard
|
||||
-- resize: mouses are hard
|
||||
-- close: mouses are hard
|
||||
--
|
||||
-- scrollbar
|
||||
-- scrollbarhighlight
|
||||
-- thumbhighlight
|
||||
|
||||
-- tabpage: seems useless
|
||||
|
||||
-- Create border
|
||||
|
||||
local should_show_border = nil
|
||||
local border_options = {}
|
||||
|
||||
-- border List with numbers, defining the border thickness
|
||||
-- above/right/below/left of the popup (similar to CSS).
|
||||
-- Only values of zero and non-zero are recognized.
|
||||
-- An empty list uses a border all around.
|
||||
if vim_options.border then
|
||||
should_show_border = true
|
||||
|
||||
if type(vim_options.border) == "boolean" or vim.tbl_isempty(vim_options.border) then
|
||||
border_options.border_thickness = Border._default_thickness
|
||||
elseif #vim_options.border == 4 then
|
||||
border_options.border_thickness = {
|
||||
top = utils.bounded(vim_options.border[1], 0, 1),
|
||||
right = utils.bounded(vim_options.border[2], 0, 1),
|
||||
bot = utils.bounded(vim_options.border[3], 0, 1),
|
||||
left = utils.bounded(vim_options.border[4], 0, 1),
|
||||
}
|
||||
else
|
||||
error(string.format("Invalid configuration for border: %s", vim.inspect(vim_options.border)))
|
||||
end
|
||||
elseif vim_options.border == false then
|
||||
should_show_border = false
|
||||
end
|
||||
|
||||
if (should_show_border == nil or should_show_border) and vim_options.borderchars then
|
||||
should_show_border = true
|
||||
|
||||
-- borderchars List with characters, defining the character to use
|
||||
-- for the top/right/bottom/left border. Optionally
|
||||
-- followed by the character to use for the
|
||||
-- topleft/topright/botright/botleft corner.
|
||||
-- Example: ['-', '|', '-', '|', '┌', '┐', '┘', '└']
|
||||
-- When the list has one character it is used for all.
|
||||
-- When the list has two characters the first is used for
|
||||
-- the border lines, the second for the corners.
|
||||
-- By default a double line is used all around when
|
||||
-- 'encoding' is "utf-8" and 'ambiwidth' is "single",
|
||||
-- otherwise ASCII characters are used.
|
||||
|
||||
local b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft
|
||||
if vim_options.borderchars == nil then
|
||||
b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft =
|
||||
"═", "║", "═", "║", "╔", "╗", "╝", "╚"
|
||||
elseif #vim_options.borderchars == 1 then
|
||||
local b_char = vim_options.borderchars[1]
|
||||
b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft =
|
||||
b_char, b_char, b_char, b_char, b_char, b_char, b_char, b_char
|
||||
elseif #vim_options.borderchars == 2 then
|
||||
local b_char = vim_options.borderchars[1]
|
||||
local c_char = vim_options.borderchars[2]
|
||||
b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft =
|
||||
b_char, b_char, b_char, b_char, c_char, c_char, c_char, c_char
|
||||
elseif #vim_options.borderchars == 8 then
|
||||
b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = unpack(vim_options.borderchars)
|
||||
else
|
||||
error(string.format 'Not enough arguments for "borderchars"')
|
||||
end
|
||||
|
||||
border_options.top = b_top
|
||||
border_options.bot = b_bot
|
||||
border_options.right = b_right
|
||||
border_options.left = b_left
|
||||
border_options.topleft = b_topleft
|
||||
border_options.topright = b_topright
|
||||
border_options.botright = b_botright
|
||||
border_options.botleft = b_botleft
|
||||
end
|
||||
|
||||
-- title
|
||||
if vim_options.title then
|
||||
-- TODO: Check out how title works with weird border combos.
|
||||
border_options.title = vim_options.title
|
||||
end
|
||||
|
||||
local border = nil
|
||||
if should_show_border then
|
||||
border_options.focusable = vim_options.border_focusable
|
||||
border_options.highlight = vim_options.borderhighlight and string.format("Normal:%s", vim_options.borderhighlight)
|
||||
border_options.titlehighlight = vim_options.titlehighlight
|
||||
border = Border:new(bufnr, win_id, win_opts, border_options)
|
||||
popup._borders[win_id] = border
|
||||
end
|
||||
|
||||
if vim_options.highlight then
|
||||
vim.api.nvim_win_set_option(
|
||||
win_id,
|
||||
"winhl",
|
||||
string.format("Normal:%s,EndOfBuffer:%s", vim_options.highlight, vim_options.highlight)
|
||||
)
|
||||
end
|
||||
|
||||
-- enter
|
||||
local should_enter = vim_options.enter
|
||||
if should_enter == nil then
|
||||
should_enter = true
|
||||
end
|
||||
|
||||
if should_enter then
|
||||
-- set focus after border creation so that it's properly placed (especially
|
||||
-- in relative cursor layout)
|
||||
if vim_options.noautocmd then
|
||||
vim.cmd("noautocmd lua vim.api.nvim_set_current_win(" .. win_id .. ")")
|
||||
else
|
||||
vim.api.nvim_set_current_win(win_id)
|
||||
end
|
||||
end
|
||||
|
||||
-- callback
|
||||
if vim_options.callback then
|
||||
popup._callbacks[bufnr] = function()
|
||||
-- (jbyuki): Giving win_id is pointless here because it's closed right afterwards
|
||||
-- but it might make more sense once hidden is implemented
|
||||
local row, _ = unpack(vim.api.nvim_win_get_cursor(win_id))
|
||||
vim_options.callback(win_id, what[row])
|
||||
vim.api.nvim_win_close(win_id, true)
|
||||
end
|
||||
vim.api.nvim_buf_set_keymap(
|
||||
bufnr,
|
||||
"n",
|
||||
"<CR>",
|
||||
'<cmd>lua require"plenary.popup".execute_callback(' .. bufnr .. ")<CR>",
|
||||
{ noremap = true }
|
||||
)
|
||||
end
|
||||
|
||||
if vim_options.finalize_callback then
|
||||
vim_options.finalize_callback(win_id, bufnr)
|
||||
end
|
||||
|
||||
-- TODO: Perhaps there's a way to return an object that looks like a window id,
|
||||
-- but actually has some extra metadata about it.
|
||||
--
|
||||
-- This would make `hidden` a lot easier to manage
|
||||
return win_id, {
|
||||
win_id = win_id,
|
||||
border = border,
|
||||
}
|
||||
end
|
||||
|
||||
-- Move popup with window id {win_id} to the position specified with {vim_options}.
|
||||
-- {vim_options} may contain the following items that determine the popup position/size:
|
||||
-- - line
|
||||
-- - col
|
||||
-- - height
|
||||
-- - width
|
||||
-- - maxheight/minheight
|
||||
-- - maxwidth/minwidth
|
||||
-- - pos
|
||||
-- Unimplemented vim options here include: fixed
|
||||
function popup.move(win_id, vim_options)
|
||||
-- Create win_options
|
||||
local win_opts = {}
|
||||
win_opts.relative = "editor"
|
||||
|
||||
local current_pos = vim.api.nvim_win_get_position(win_id)
|
||||
local default_opts = {
|
||||
width = vim.api.nvim_win_get_width(win_id),
|
||||
height = vim.api.nvim_win_get_height(win_id),
|
||||
row = current_pos[1],
|
||||
col = current_pos[2],
|
||||
}
|
||||
|
||||
-- Add positional and sizing config to win_opts
|
||||
add_position_config(win_opts, vim_options, default_opts)
|
||||
|
||||
-- Update content window
|
||||
vim.api.nvim_win_set_config(win_id, win_opts)
|
||||
|
||||
-- Update border window (if present)
|
||||
local border = popup._borders[win_id]
|
||||
if border ~= nil then
|
||||
border:move(win_opts, border._border_win_options)
|
||||
end
|
||||
end
|
||||
|
||||
function popup.execute_callback(bufnr)
|
||||
if popup._callbacks[bufnr] then
|
||||
local wrapper = popup._callbacks[bufnr]
|
||||
wrapper()
|
||||
popup._callbacks[bufnr] = nil
|
||||
end
|
||||
end
|
||||
|
||||
return popup
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
local utils = {}
|
||||
|
||||
utils.bounded = function(value, min, max)
|
||||
min = min or 0
|
||||
max = max or math.huge
|
||||
|
||||
if min then
|
||||
value = math.max(value, min)
|
||||
end
|
||||
if max then
|
||||
value = math.min(value, max)
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
utils.apply_defaults = function(original, defaults)
|
||||
if original == nil then
|
||||
original = {}
|
||||
end
|
||||
|
||||
original = vim.deepcopy(original)
|
||||
|
||||
for k, v in pairs(defaults) do
|
||||
if original[k] == nil then
|
||||
original[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
return original
|
||||
end
|
||||
|
||||
return utils
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
local profile = {}
|
||||
|
||||
-- bundled version of upstream jit.p until LuaJIT is updated to include
|
||||
-- https://github.com/LuaJIT/LuaJIT/commit/95140c50010c0557af66dac944403a1a65dd312c
|
||||
local p = require'plenary.profile.p'
|
||||
|
||||
---start profiling using LuaJIT profiler
|
||||
---@param out name and path of log file
|
||||
---@param opts table of options
|
||||
--- flame (bool, default false) write log in flamegraph format
|
||||
-- (see https://github.com/jonhoo/inferno)
|
||||
function profile.start(out, opts)
|
||||
out = out or "profile.log"
|
||||
opts = opts or {}
|
||||
local popts = "10,i1,s,m0"
|
||||
if opts.flame then popts = popts .. ",G" end
|
||||
p.start(popts, out)
|
||||
end
|
||||
|
||||
---stop profiling
|
||||
profile.stop = p.stop
|
||||
|
||||
function profile.benchmark(iterations, f, ...)
|
||||
local start_time = vim.loop.hrtime()
|
||||
for _ = 1, iterations do
|
||||
f(...)
|
||||
end
|
||||
return (vim.loop.hrtime() - start_time) / 1E9
|
||||
end
|
||||
|
||||
return profile
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
--[[ Copyright (c) 2018-2020, Charles Mallah ]]
|
||||
-- Released with MIT License
|
||||
--
|
||||
-- Originally link:
|
||||
-- https://github.com/charlesmallah/lua-profiler
|
||||
--
|
||||
-- Hopefully will add some better neovim stuff in the future.
|
||||
-- Shoutout to @clason for finding this.
|
||||
|
||||
|
||||
---------------------------------------|
|
||||
--- Configuration
|
||||
--
|
||||
---------------------------------------|
|
||||
|
||||
local PROFILER_FILENAME = "lua/telescope/profile/lua_profiler.lua" -- Location and name of profiler (to remove itself from reports);
|
||||
-- e.g. if this is in a 'tool' folder, rename this as: "tool/profiler.lua"
|
||||
|
||||
local EMPTY_TIME = "0.0000" -- Detect empty time, replace with tag below
|
||||
local emptyToThis = "~"
|
||||
|
||||
local fileWidth = 75
|
||||
local funcWidth = 22
|
||||
local lineWidth = 6
|
||||
local timeWidth = 7
|
||||
local relaWidth = 6
|
||||
local callWidth = 4
|
||||
|
||||
local reportSaved = " > Report saved to"
|
||||
local formatOutputHeader = "| %-"..fileWidth.."s: %-"..funcWidth.."s: %-"..lineWidth.."s: %-"..timeWidth.."s: %-"..relaWidth.."s: %-"..callWidth.."s|\n"
|
||||
local formatOutputTitle = "%-"..fileWidth.."."..fileWidth.."s: %-"..funcWidth.."."..funcWidth.."s: %-"..lineWidth.."s" -- File / Function / Line count
|
||||
local formatOutput = "| %s: %-"..timeWidth.."s: %-"..relaWidth.."s: %-"..callWidth.."s|\n" -- Time / Relative / Called
|
||||
local formatTotalTime = "TOTAL TIME = %f s\n"
|
||||
local formatFunLine = "%"..(lineWidth - 2).."i"
|
||||
local formatFunTime = "%04.4f"
|
||||
local formatFunRelative = "%03.1f"
|
||||
local formatFunCount = "%"..(callWidth - 1).."i"
|
||||
local formatHeader = string.format(formatOutputHeader, "FILE", "FUNCTION", "LINE", "TIME", "%", "#")
|
||||
|
||||
|
||||
---------------------------------------|
|
||||
--- Locals
|
||||
--
|
||||
---------------------------------------|
|
||||
|
||||
local module = {}
|
||||
|
||||
local getTime = os.clock
|
||||
local string = string
|
||||
local debug = debug
|
||||
local table = table
|
||||
|
||||
local TABL_REPORT_CACHE = {}
|
||||
local TABL_REPORTS = {}
|
||||
local reportCount = 0
|
||||
local startTime = 0
|
||||
local stopTime = 0
|
||||
|
||||
local printFun = nil
|
||||
local verbosePrint = false
|
||||
|
||||
local function functionReport(information)
|
||||
local src = information.short_src
|
||||
if src == nil then
|
||||
src = "<C>"
|
||||
elseif string.sub(src, #src - 3, #src) == ".lua" then
|
||||
src = string.sub(src, 1, #src - 4)
|
||||
end
|
||||
|
||||
local name = information.name
|
||||
if name == nil then
|
||||
name = "Anon"
|
||||
elseif string.sub(name, #name - 1, #name) == "_l" then
|
||||
name = string.sub(name, 1, #name - 2)
|
||||
end
|
||||
|
||||
local title = string.format(formatOutputTitle,
|
||||
src, name,
|
||||
string.format(formatFunLine, information.linedefined or 0))
|
||||
|
||||
local funcReport = TABL_REPORT_CACHE[title]
|
||||
if not funcReport then
|
||||
funcReport = {
|
||||
title = string.format(formatOutputTitle,
|
||||
src, name,
|
||||
string.format(formatFunLine, information.linedefined or 0)),
|
||||
count = 0,
|
||||
timer = 0,
|
||||
}
|
||||
TABL_REPORT_CACHE[title] = funcReport
|
||||
reportCount = reportCount + 1
|
||||
TABL_REPORTS[reportCount] = funcReport
|
||||
end
|
||||
|
||||
return funcReport
|
||||
end
|
||||
|
||||
local onDebugHook = function(hookType)
|
||||
local information = debug.getinfo(2, "nS")
|
||||
if hookType == "call" then
|
||||
local funcReport = functionReport(information)
|
||||
funcReport.callTime = getTime()
|
||||
funcReport.count = funcReport.count + 1
|
||||
elseif hookType == "return" then
|
||||
local funcReport = functionReport(information)
|
||||
if funcReport.callTime and funcReport.count > 0 then
|
||||
funcReport.timer = funcReport.timer + (getTime() - funcReport.callTime)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function charRepetition(n, character)
|
||||
local s = ""
|
||||
character = character or " "
|
||||
for _ = 1, n do
|
||||
s = s..character
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
local function singleSearchReturn(str, search)
|
||||
for _ in string.gmatch(str, search) do
|
||||
do return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local divider = charRepetition(#formatHeader - 1, "-").."\n"
|
||||
|
||||
|
||||
---------------------------------------|
|
||||
--- Functions
|
||||
--
|
||||
---------------------------------------|
|
||||
|
||||
--- Attach a print function to the profiler, to receive a single string parameter
|
||||
--
|
||||
function module.attachPrintFunction(fn, verbose)
|
||||
printFun = fn
|
||||
if verbose ~= nil then
|
||||
verbosePrint = verbose
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
--
|
||||
function module.start()
|
||||
TABL_REPORT_CACHE = {}
|
||||
TABL_REPORTS = {}
|
||||
reportCount = 0
|
||||
startTime = getTime()
|
||||
stopTime = nil
|
||||
debug.sethook(onDebugHook, "cr", 0)
|
||||
end
|
||||
|
||||
---
|
||||
--
|
||||
function module.stop()
|
||||
stopTime = getTime()
|
||||
debug.sethook()
|
||||
end
|
||||
|
||||
--- Writes the profile report to file
|
||||
--
|
||||
function module.report(filename)
|
||||
if stopTime == nil then
|
||||
module.stop()
|
||||
end
|
||||
|
||||
if reportCount > 0 then
|
||||
filename = filename or "profiler.log"
|
||||
table.sort(TABL_REPORTS, function(a, b) return a.timer > b.timer end)
|
||||
local file = io.open(filename, "w+")
|
||||
|
||||
if reportCount > 0 then
|
||||
local divide = false
|
||||
local totalTime = stopTime - startTime
|
||||
local totalTimeOutput = " > "..string.format(formatTotalTime, totalTime)
|
||||
|
||||
file:write(totalTimeOutput)
|
||||
if printFun ~= nil then
|
||||
printFun(totalTimeOutput)
|
||||
end
|
||||
|
||||
file:write("\n"..divider)
|
||||
file:write(formatHeader)
|
||||
file:write(divider)
|
||||
|
||||
for i = 1, reportCount do
|
||||
local funcReport = TABL_REPORTS[i]
|
||||
|
||||
if funcReport.count > 0 and funcReport.timer <= totalTime then
|
||||
local printThis = true
|
||||
|
||||
if PROFILER_FILENAME ~= "" then
|
||||
if singleSearchReturn(funcReport.title, PROFILER_FILENAME) then
|
||||
printThis = false
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove line if not needed
|
||||
if printThis == true then
|
||||
if singleSearchReturn(funcReport.title, "[[C]]") then
|
||||
printThis = false
|
||||
end
|
||||
end
|
||||
|
||||
if printThis == true then
|
||||
local count = string.format(formatFunCount, funcReport.count)
|
||||
local timer = string.format(formatFunTime, funcReport.timer)
|
||||
local relTime = string.format(formatFunRelative, (funcReport.timer / totalTime) * 100)
|
||||
if divide == false and timer == EMPTY_TIME then
|
||||
file:write(divider)
|
||||
divide = true
|
||||
end
|
||||
|
||||
-- Replace
|
||||
if timer == EMPTY_TIME then
|
||||
timer = emptyToThis
|
||||
relTime = emptyToThis
|
||||
end
|
||||
|
||||
-- Build final line
|
||||
local outputLine = string.format(formatOutput, funcReport.title, timer, relTime, count)
|
||||
file:write(outputLine)
|
||||
|
||||
-- This is a verbose print to the printFun, however maybe make this smaller for on screen debug?
|
||||
if printFun ~= nil and verbosePrint == true then
|
||||
printFun(outputLine)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
file:write(divider)
|
||||
|
||||
end
|
||||
|
||||
file:close()
|
||||
|
||||
if printFun ~= nil then
|
||||
printFun(reportSaved.."'"..filename.."'")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- End
|
||||
--
|
||||
return module
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,312 @@
|
|||
----------------------------------------------------------------------------
|
||||
-- LuaJIT profiler.
|
||||
--
|
||||
-- Copyright (C) 2005-2021 Mike Pall. All rights reserved.
|
||||
-- Released under the MIT license. See Copyright Notice in luajit.h
|
||||
----------------------------------------------------------------------------
|
||||
--
|
||||
-- This module is a simple command line interface to the built-in
|
||||
-- low-overhead profiler of LuaJIT.
|
||||
--
|
||||
-- The lower-level API of the profiler is accessible via the "jit.profile"
|
||||
-- module or the luaJIT_profile_* C API.
|
||||
--
|
||||
-- Example usage:
|
||||
--
|
||||
-- luajit -jp myapp.lua
|
||||
-- luajit -jp=s myapp.lua
|
||||
-- luajit -jp=-s myapp.lua
|
||||
-- luajit -jp=vl myapp.lua
|
||||
-- luajit -jp=G,profile.txt myapp.lua
|
||||
--
|
||||
-- The following dump features are available:
|
||||
--
|
||||
-- f Stack dump: function name, otherwise module:line. Default mode.
|
||||
-- F Stack dump: ditto, but always prepend module.
|
||||
-- l Stack dump: module:line.
|
||||
-- <number> stack dump depth (callee < caller). Default: 1.
|
||||
-- -<number> Inverse stack dump depth (caller > callee).
|
||||
-- s Split stack dump after first stack level. Implies abs(depth) >= 2.
|
||||
-- p Show full path for module names.
|
||||
-- v Show VM states. Can be combined with stack dumps, e.g. vf or fv.
|
||||
-- z Show zones. Can be combined with stack dumps, e.g. zf or fz.
|
||||
-- r Show raw sample counts. Default: show percentages.
|
||||
-- a Annotate excerpts from source code files.
|
||||
-- A Annotate complete source code files.
|
||||
-- G Produce raw output suitable for graphical tools (e.g. flame graphs).
|
||||
-- m<number> Minimum sample percentage to be shown. Default: 3.
|
||||
-- i<number> Sampling interval in milliseconds. Default: 10.
|
||||
--
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
-- Cache some library functions and objects.
|
||||
local jit = require("jit")
|
||||
assert(20100 <= jit.version_num and jit.version_num <= 20199, "LuaJIT core/library version mismatch: " .. jit.version)
|
||||
local profile = require("jit.profile")
|
||||
local vmdef = require("jit.vmdef")
|
||||
local math = math
|
||||
local pairs, ipairs, tonumber, floor = pairs, ipairs, tonumber, math.floor
|
||||
local sort, format = table.sort, string.format
|
||||
local stdout = io.stdout
|
||||
local zone -- Load jit.zone module on demand.
|
||||
|
||||
-- Output file handle.
|
||||
local out
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local prof_ud
|
||||
local prof_states, prof_split, prof_min, prof_raw, prof_fmt, prof_depth
|
||||
local prof_ann, prof_count1, prof_count2, prof_samples
|
||||
|
||||
local map_vmmode = {
|
||||
N = "Compiled",
|
||||
I = "Interpreted",
|
||||
C = "C code",
|
||||
G = "Garbage Collector",
|
||||
J = "JIT Compiler",
|
||||
}
|
||||
|
||||
-- Profiler callback.
|
||||
local function prof_cb(th, samples, vmmode)
|
||||
prof_samples = prof_samples + samples
|
||||
local key_stack, key_stack2, key_state
|
||||
-- Collect keys for sample.
|
||||
if prof_states then
|
||||
if prof_states == "v" then
|
||||
key_state = map_vmmode[vmmode] or vmmode
|
||||
else
|
||||
key_state = zone:get() or "(none)"
|
||||
end
|
||||
end
|
||||
if prof_fmt then
|
||||
key_stack = profile.dumpstack(th, prof_fmt, prof_depth)
|
||||
key_stack = key_stack:gsub("%[builtin#(%d+)%]", function(x)
|
||||
return vmdef.ffnames[tonumber(x)]
|
||||
end)
|
||||
if prof_split == 2 then
|
||||
local k1, k2 = key_stack:match("(.-) [<>] (.*)")
|
||||
if k2 then key_stack, key_stack2 = k1, k2 end
|
||||
elseif prof_split == 3 then
|
||||
key_stack2 = profile.dumpstack(th, "l", 1)
|
||||
end
|
||||
end
|
||||
-- Order keys.
|
||||
local k1, k2
|
||||
if prof_split == 1 then
|
||||
if key_state then
|
||||
k1 = key_state
|
||||
if key_stack then k2 = key_stack end
|
||||
end
|
||||
elseif key_stack then
|
||||
k1 = key_stack
|
||||
if key_stack2 then k2 = key_stack2 elseif key_state then k2 = key_state end
|
||||
end
|
||||
-- Coalesce samples in one or two levels.
|
||||
if k1 then
|
||||
local t1 = prof_count1
|
||||
t1[k1] = (t1[k1] or 0) + samples
|
||||
if k2 then
|
||||
local t2 = prof_count2
|
||||
local t3 = t2[k1]
|
||||
if not t3 then t3 = {}; t2[k1] = t3 end
|
||||
t3[k2] = (t3[k2] or 0) + samples
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Show top N list.
|
||||
local function prof_top(count1, count2, samples, indent)
|
||||
local t, n = {}, 0
|
||||
for k in pairs(count1) do
|
||||
n = n + 1
|
||||
t[n] = k
|
||||
end
|
||||
sort(t, function(a, b) return count1[a] > count1[b] end)
|
||||
for i=1,n do
|
||||
local k = t[i]
|
||||
local v = count1[k]
|
||||
local pct = floor(v*100/samples + 0.5)
|
||||
if pct < prof_min then break end
|
||||
if not prof_raw then
|
||||
out:write(format("%s%2d%% %s\n", indent, pct, k))
|
||||
elseif prof_raw == "r" then
|
||||
out:write(format("%s%5d %s\n", indent, v, k))
|
||||
else
|
||||
out:write(format("%s %d\n", k, v))
|
||||
end
|
||||
if count2 then
|
||||
local r = count2[k]
|
||||
if r then
|
||||
prof_top(r, nil, v, (prof_split == 3 or prof_split == 1) and " -- " or
|
||||
(prof_depth < 0 and " -> " or " <- "))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Annotate source code
|
||||
local function prof_annotate(count1, samples)
|
||||
local files = {}
|
||||
local ms = 0
|
||||
for k, v in pairs(count1) do
|
||||
local pct = floor(v*100/samples + 0.5)
|
||||
ms = math.max(ms, v)
|
||||
if pct >= prof_min then
|
||||
local file, line = k:match("^(.*):(%d+)$")
|
||||
if not file then file = k; line = 0 end
|
||||
local fl = files[file]
|
||||
if not fl then fl = {}; files[file] = fl; files[#files+1] = file end
|
||||
line = tonumber(line)
|
||||
fl[line] = prof_raw and v or pct
|
||||
end
|
||||
end
|
||||
sort(files)
|
||||
local fmtv, fmtn = " %3d%% | %s\n", " | %s\n"
|
||||
if prof_raw then
|
||||
local n = math.max(5, math.ceil(math.log10(ms)))
|
||||
fmtv = "%"..n.."d | %s\n"
|
||||
fmtn = (" "):rep(n).." | %s\n"
|
||||
end
|
||||
local ann = prof_ann
|
||||
for _, file in ipairs(files) do
|
||||
local f0 = file:byte()
|
||||
if f0 == 40 or f0 == 91 then
|
||||
out:write(format("\n====== %s ======\n[Cannot annotate non-file]\n", file))
|
||||
break
|
||||
end
|
||||
local fp, err = io.open(file)
|
||||
if not fp then
|
||||
out:write(format("====== ERROR: %s: %s\n", file, err))
|
||||
break
|
||||
end
|
||||
out:write(format("\n====== %s ======\n", file))
|
||||
local fl = files[file]
|
||||
local n, show = 1, false
|
||||
if ann ~= 0 then
|
||||
for i=1,ann do
|
||||
if fl[i] then show = true; out:write("@@ 1 @@\n"); break end
|
||||
end
|
||||
end
|
||||
for line in fp:lines() do
|
||||
if line:byte() == 27 then
|
||||
out:write("[Cannot annotate bytecode file]\n")
|
||||
break
|
||||
end
|
||||
local v = fl[n]
|
||||
if ann ~= 0 then
|
||||
local v2 = fl[n+ann]
|
||||
if show then
|
||||
if v2 then show = n+ann elseif v then show = n
|
||||
elseif show+ann < n then show = false end
|
||||
elseif v2 then
|
||||
show = n+ann
|
||||
out:write(format("@@ %d @@\n", n))
|
||||
end
|
||||
if not show then goto next end
|
||||
end
|
||||
if v then
|
||||
out:write(format(fmtv, v, line))
|
||||
else
|
||||
out:write(format(fmtn, line))
|
||||
end
|
||||
::next::
|
||||
n = n + 1
|
||||
end
|
||||
fp:close()
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Finish profiling and dump result.
|
||||
local function prof_finish()
|
||||
if prof_ud then
|
||||
profile.stop()
|
||||
local samples = prof_samples
|
||||
if samples == 0 then
|
||||
if prof_raw ~= true then out:write("[No samples collected]\n") end
|
||||
return
|
||||
end
|
||||
if prof_ann then
|
||||
prof_annotate(prof_count1, samples)
|
||||
else
|
||||
prof_top(prof_count1, prof_count2, samples, "")
|
||||
end
|
||||
prof_count1 = nil
|
||||
prof_count2 = nil
|
||||
prof_ud = nil
|
||||
if out ~= stdout then out:close() end
|
||||
end
|
||||
end
|
||||
|
||||
-- Start profiling.
|
||||
local function prof_start(mode)
|
||||
local interval = ""
|
||||
mode = mode:gsub("i%d*", function(s) interval = s; return "" end)
|
||||
prof_min = 3
|
||||
mode = mode:gsub("m(%d+)", function(s) prof_min = tonumber(s); return "" end)
|
||||
prof_depth = 1
|
||||
mode = mode:gsub("%-?%d+", function(s) prof_depth = tonumber(s); return "" end)
|
||||
local m = {}
|
||||
for c in mode:gmatch(".") do m[c] = c end
|
||||
prof_states = m.z or m.v
|
||||
if prof_states == "z" then zone = require("jit.zone") end
|
||||
local scope = m.l or m.f or m.F or (prof_states and "" or "f")
|
||||
local flags = (m.p or "")
|
||||
prof_raw = m.r
|
||||
if m.s then
|
||||
prof_split = 2
|
||||
if prof_depth == -1 or m["-"] then prof_depth = -2
|
||||
elseif prof_depth == 1 then prof_depth = 2 end
|
||||
elseif mode:find("[fF].*l") then
|
||||
scope = "l"
|
||||
prof_split = 3
|
||||
else
|
||||
prof_split = (scope == "" or mode:find("[zv].*[lfF]")) and 1 or 0
|
||||
end
|
||||
prof_ann = m.A and 0 or (m.a and 3)
|
||||
if prof_ann then
|
||||
scope = "l"
|
||||
prof_fmt = "pl"
|
||||
prof_split = 0
|
||||
prof_depth = 1
|
||||
elseif m.G and scope ~= "" then
|
||||
prof_fmt = flags..scope.."Z;"
|
||||
prof_depth = -100
|
||||
prof_raw = true
|
||||
prof_min = 0
|
||||
elseif scope == "" then
|
||||
prof_fmt = false
|
||||
else
|
||||
local sc = prof_split == 3 and m.f or m.F or scope
|
||||
prof_fmt = flags..sc..(prof_depth >= 0 and "Z < " or "Z > ")
|
||||
end
|
||||
prof_count1 = {}
|
||||
prof_count2 = {}
|
||||
prof_samples = 0
|
||||
profile.start(scope:lower()..interval, prof_cb)
|
||||
prof_ud = newproxy(true)
|
||||
getmetatable(prof_ud).__gc = prof_finish
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local function start(mode, outfile)
|
||||
if not outfile then outfile = os.getenv("LUAJIT_PROFILEFILE") end
|
||||
if outfile then
|
||||
out = outfile == "-" and stdout or assert(io.open(outfile, "w"))
|
||||
else
|
||||
out = stdout
|
||||
end
|
||||
prof_start(mode or "f")
|
||||
end
|
||||
|
||||
-- Public module functions.
|
||||
return {
|
||||
start = start, -- For -j command line option.
|
||||
stop = prof_finish
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
local reload = {}
|
||||
|
||||
reload.reload_module = function(module_name, starts_with_only)
|
||||
-- Default to starts with only
|
||||
if starts_with_only == nil then
|
||||
starts_with_only = true
|
||||
end
|
||||
|
||||
-- TODO: Might need to handle cpath / compiled lua packages? Not sure.
|
||||
local matcher
|
||||
if not starts_with_only then
|
||||
matcher = function(pack)
|
||||
return string.find(pack, module_name, 1, true)
|
||||
end
|
||||
else
|
||||
local module_name_pattern = vim.pesc(module_name)
|
||||
matcher = function(pack)
|
||||
return string.find(pack, "^" .. module_name_pattern)
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle impatient.nvim automatically.
|
||||
local luacache = (_G.__luacache or {}).cache
|
||||
|
||||
for pack, _ in pairs(package.loaded) do
|
||||
if matcher(pack) then
|
||||
package.loaded[pack] = nil
|
||||
|
||||
if luacache then
|
||||
luacache[pack] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return reload
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
local floatwin = require "plenary.window.float"
|
||||
|
||||
local run = {}
|
||||
|
||||
run.with_displayed_output = function(title_text, cmd, opts)
|
||||
local views = floatwin.centered_with_top_win(title_text)
|
||||
|
||||
local job_id = vim.fn.termopen(cmd)
|
||||
|
||||
local count = 0
|
||||
while not vim.wait(1000, function()
|
||||
return vim.fn.jobwait({ job_id }, 0)[1] == -1
|
||||
end) do
|
||||
vim.cmd [[normal! G]]
|
||||
count = count + 1
|
||||
|
||||
if count == 10 then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
vim.fn.win_gotoid(views.win_id)
|
||||
vim.cmd [[startinsert]]
|
||||
|
||||
return views.bufnr, views.win_id
|
||||
end
|
||||
|
||||
return run
|
||||
|
|
@ -0,0 +1,620 @@
|
|||
local Path = require "plenary.path"
|
||||
local os_sep = Path.path.sep
|
||||
local F = require "plenary.functional"
|
||||
local compat = require "plenary.compat"
|
||||
|
||||
local uv = vim.loop
|
||||
|
||||
local m = {}
|
||||
|
||||
local make_gitignore = function(basepath)
|
||||
local patterns = {}
|
||||
local valid = false
|
||||
for _, v in ipairs(basepath) do
|
||||
local p = Path:new(v .. os_sep .. ".gitignore")
|
||||
if p:exists() then
|
||||
valid = true
|
||||
patterns[v] = { ignored = {}, negated = {} }
|
||||
for l in p:iter() do
|
||||
local prefix = l:sub(1, 1)
|
||||
local negated = prefix == "!"
|
||||
if negated then
|
||||
l = l:sub(2)
|
||||
prefix = l:sub(1, 1)
|
||||
end
|
||||
if prefix == "/" then
|
||||
l = v .. l
|
||||
end
|
||||
if not (prefix == "" or prefix == "#") then
|
||||
local el = vim.trim(l)
|
||||
el = el:gsub("%-", "%%-")
|
||||
el = el:gsub("%.", "%%.")
|
||||
el = el:gsub("/%*%*/", "/%%w+/")
|
||||
el = el:gsub("%*%*", "")
|
||||
el = el:gsub("%*", "%%w+")
|
||||
el = el:gsub("%?", "%%w")
|
||||
if el ~= "" then
|
||||
table.insert(negated and patterns[v].negated or patterns[v].ignored, el)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not valid then
|
||||
return nil
|
||||
end
|
||||
return function(bp, entry)
|
||||
for _, v in ipairs(bp) do
|
||||
if entry:find(v, 1, true) then
|
||||
local negated = false
|
||||
for _, w in ipairs(patterns[v].ignored) do
|
||||
if not negated and entry:match(w) then
|
||||
for _, inverse in ipairs(patterns[v].negated) do
|
||||
if not negated and entry:match(inverse) then
|
||||
negated = true
|
||||
end
|
||||
end
|
||||
if not negated then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
-- exposed for testing
|
||||
m.__make_gitignore = make_gitignore
|
||||
|
||||
local handle_depth = function(base_paths, entry, depth)
|
||||
for _, v in ipairs(base_paths) do
|
||||
if entry:find(v, 1, true) then
|
||||
local cut = entry:sub(#v + 1, -1)
|
||||
cut = cut:sub(1, 1) == os_sep and cut:sub(2, -1) or cut
|
||||
local _, count = cut:gsub(os_sep, "")
|
||||
if depth <= (count + 1) then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
return entry
|
||||
end
|
||||
|
||||
local gen_search_pat = function(pattern)
|
||||
if type(pattern) == "string" then
|
||||
return function(entry)
|
||||
return entry:match(pattern)
|
||||
end
|
||||
elseif type(pattern) == "table" then
|
||||
return function(entry)
|
||||
for _, v in ipairs(pattern) do
|
||||
if entry:match(v) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
elseif type(pattern) == "function" then
|
||||
return pattern
|
||||
end
|
||||
end
|
||||
|
||||
local process_item = function(opts, name, typ, current_dir, next_dir, bp, data, giti, msp)
|
||||
if opts.hidden or name:sub(1, 1) ~= "." then
|
||||
if typ == "directory" then
|
||||
local entry = current_dir .. os_sep .. name
|
||||
if opts.depth then
|
||||
table.insert(next_dir, handle_depth(bp, entry, opts.depth))
|
||||
else
|
||||
table.insert(next_dir, entry)
|
||||
end
|
||||
if opts.add_dirs or opts.only_dirs then
|
||||
if not giti or giti(bp, entry .. "/") then
|
||||
if not msp or msp(entry) then
|
||||
table.insert(data, entry)
|
||||
if opts.on_insert then
|
||||
opts.on_insert(entry, typ)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif not opts.only_dirs then
|
||||
local entry = current_dir .. os_sep .. name
|
||||
if not giti or giti(bp, entry) then
|
||||
if not msp or msp(entry) then
|
||||
table.insert(data, entry)
|
||||
if opts.on_insert then
|
||||
opts.on_insert(entry, typ)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- m.scan_dir
|
||||
-- Search directory recursive and syncronous
|
||||
-- @param path: string or table
|
||||
-- string has to be a valid path
|
||||
-- table has to be a array of valid paths
|
||||
-- @param opts: table to change behavior
|
||||
-- opts.hidden (bool): if true hidden files will be added
|
||||
-- opts.add_dirs (bool): if true dirs will also be added to the results
|
||||
-- opts.only_dirs (bool): if true only dirs will be added to the results
|
||||
-- opts.respect_gitignore (bool): if true will only add files that are not ignored by the git
|
||||
-- opts.depth (int): depth on how deep the search should go
|
||||
-- opts.search_pattern (regex): regex for which files will be added, string, table of strings, or fn(e) -> bool
|
||||
-- opts.on_insert(entry): Will be called for each element
|
||||
-- opts.silent (bool): if true will not echo messages that are not accessible
|
||||
-- @return array with files
|
||||
m.scan_dir = function(path, opts)
|
||||
opts = opts or {}
|
||||
|
||||
local data = {}
|
||||
local base_paths = compat.flatten { path }
|
||||
local next_dir = compat.flatten { path }
|
||||
|
||||
local gitignore = opts.respect_gitignore and make_gitignore(base_paths) or nil
|
||||
local match_search_pat = opts.search_pattern and gen_search_pat(opts.search_pattern) or nil
|
||||
|
||||
for i = #base_paths, 1, -1 do
|
||||
if uv.fs_access(base_paths[i], "X") == false then
|
||||
if not F.if_nil(opts.silent, false, opts.silent) then
|
||||
print(string.format("%s is not accessible by the current user!", base_paths[i]))
|
||||
end
|
||||
table.remove(base_paths, i)
|
||||
end
|
||||
end
|
||||
if #base_paths == 0 then
|
||||
return {}
|
||||
end
|
||||
|
||||
repeat
|
||||
local current_dir = table.remove(next_dir, 1)
|
||||
local fd = uv.fs_scandir(current_dir)
|
||||
if fd then
|
||||
while true do
|
||||
local name, typ = uv.fs_scandir_next(fd)
|
||||
if name == nil then
|
||||
break
|
||||
end
|
||||
process_item(opts, name, typ, current_dir, next_dir, base_paths, data, gitignore, match_search_pat)
|
||||
end
|
||||
end
|
||||
until #next_dir == 0
|
||||
return data
|
||||
end
|
||||
|
||||
--- m.scan_dir_async
|
||||
-- Search directory recursive and asyncronous
|
||||
-- @param path: string or table
|
||||
-- string has to be a valid path
|
||||
-- table has to be a array of valid paths
|
||||
-- @param opts: table to change behavior
|
||||
-- opts.hidden (bool): if true hidden files will be added
|
||||
-- opts.add_dirs (bool): if true dirs will also be added to the results
|
||||
-- opts.only_dirs (bool): if true only dirs will be added to the results
|
||||
-- opts.respect_gitignore (bool): if true will only add files that are not ignored by git
|
||||
-- opts.depth (int): depth on how deep the search should go
|
||||
-- opts.search_pattern (regex): regex for which files will be added, string, table of strings, or fn(e) -> bool
|
||||
-- opts.on_insert function(entry): will be called for each element
|
||||
-- opts.on_exit function(results): will be called at the end
|
||||
-- opts.silent (bool): if true will not echo messages that are not accessible
|
||||
m.scan_dir_async = function(path, opts)
|
||||
opts = opts or {}
|
||||
|
||||
local data = {}
|
||||
local base_paths = compat.flatten { path }
|
||||
local next_dir = compat.flatten { path }
|
||||
local current_dir = table.remove(next_dir, 1)
|
||||
|
||||
-- TODO(conni2461): get gitignore is not async
|
||||
local gitignore = opts.respect_gitignore and make_gitignore(base_paths) or nil
|
||||
local match_search_pat = opts.search_pattern and gen_search_pat(opts.search_pattern) or nil
|
||||
|
||||
-- TODO(conni2461): is not async. Shouldn't be that big of a problem but still
|
||||
-- Maybe obers async pr can take me out of callback hell
|
||||
for i = #base_paths, 1, -1 do
|
||||
if uv.fs_access(base_paths[i], "X") == false then
|
||||
if not F.if_nil(opts.silent, false, opts.silent) then
|
||||
print(string.format("%s is not accessible by the current user!", base_paths[i]))
|
||||
end
|
||||
table.remove(base_paths, i)
|
||||
end
|
||||
end
|
||||
if #base_paths == 0 then
|
||||
return {}
|
||||
end
|
||||
|
||||
local read_dir
|
||||
read_dir = function(err, fd)
|
||||
if not err then
|
||||
while true do
|
||||
local name, typ = uv.fs_scandir_next(fd)
|
||||
if name == nil then
|
||||
break
|
||||
end
|
||||
process_item(opts, name, typ, current_dir, next_dir, base_paths, data, gitignore, match_search_pat)
|
||||
end
|
||||
if #next_dir == 0 then
|
||||
if opts.on_exit then
|
||||
opts.on_exit(data)
|
||||
end
|
||||
else
|
||||
current_dir = table.remove(next_dir, 1)
|
||||
uv.fs_scandir(current_dir, read_dir)
|
||||
end
|
||||
end
|
||||
end
|
||||
uv.fs_scandir(current_dir, read_dir)
|
||||
end
|
||||
|
||||
local gen_permissions = (function()
|
||||
local conv_to_octal = function(nr)
|
||||
local octal, i = 0, 1
|
||||
|
||||
while nr ~= 0 do
|
||||
octal = octal + (nr % 8) * i
|
||||
nr = math.floor(nr / 8)
|
||||
i = i * 10
|
||||
end
|
||||
|
||||
return octal
|
||||
end
|
||||
|
||||
local type_tbl = { [1] = "p", [2] = "c", [4] = "d", [6] = "b", [10] = ".", [12] = "l", [14] = "s" }
|
||||
local permissions_tbl = { [0] = "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx" }
|
||||
local bit_tbl = { 4, 2, 1 }
|
||||
|
||||
return function(cache, mode)
|
||||
if cache[mode] then
|
||||
return cache[mode]
|
||||
end
|
||||
|
||||
local octal = string.format("%6d", conv_to_octal(mode))
|
||||
local l4 = octal:sub(#octal - 3, -1)
|
||||
local bit = tonumber(l4:sub(1, 1))
|
||||
|
||||
local result = type_tbl[tonumber(octal:sub(1, 2))] or "-"
|
||||
for i = 2, #l4 do
|
||||
result = result .. permissions_tbl[tonumber(l4:sub(i, i))]
|
||||
if bit - bit_tbl[i - 1] >= 0 then
|
||||
result = result:sub(1, -2) .. (bit_tbl[i - 1] == 1 and "T" or "S")
|
||||
bit = bit - bit_tbl[i - 1]
|
||||
end
|
||||
end
|
||||
|
||||
cache[mode] = result
|
||||
return result
|
||||
end
|
||||
end)()
|
||||
|
||||
local gen_size = (function()
|
||||
local size_types = { "", "K", "M", "G", "T", "P", "E", "Z" }
|
||||
|
||||
return function(size)
|
||||
-- TODO(conni2461): If type directory we could just return 4.0K
|
||||
for _, v in ipairs(size_types) do
|
||||
if math.abs(size) < 1024.0 then
|
||||
if math.abs(size) > 9 then
|
||||
return string.format("%3d%s", size, v)
|
||||
else
|
||||
return string.format("%3.1f%s", size, v)
|
||||
end
|
||||
end
|
||||
size = size / 1024.0
|
||||
end
|
||||
return string.format("%.1f%s", size, "Y")
|
||||
end
|
||||
end)()
|
||||
|
||||
local gen_date = (function()
|
||||
local current_year = os.date "%Y"
|
||||
return function(mtime)
|
||||
if current_year ~= os.date("%Y", mtime) then
|
||||
return os.date("%b %d %Y", mtime)
|
||||
end
|
||||
return os.date("%b %d %H:%M", mtime)
|
||||
end
|
||||
end)()
|
||||
|
||||
local get_username = (function()
|
||||
local fallback = function(tbl, id)
|
||||
if not tbl then
|
||||
return id
|
||||
end
|
||||
if tbl[id] then
|
||||
return tbl[id]
|
||||
end
|
||||
tbl[id] = tostring(id)
|
||||
return id
|
||||
end
|
||||
|
||||
if jit and os_sep ~= "\\" then
|
||||
local ffi = require "ffi"
|
||||
ffi.cdef [[
|
||||
typedef unsigned int __uid_t;
|
||||
typedef __uid_t uid_t;
|
||||
typedef unsigned int __gid_t;
|
||||
typedef __gid_t gid_t;
|
||||
|
||||
typedef struct {
|
||||
char *pw_name;
|
||||
char *pw_passwd;
|
||||
__uid_t pw_uid;
|
||||
__gid_t pw_gid;
|
||||
char *pw_gecos;
|
||||
char *pw_dir;
|
||||
char *pw_shell;
|
||||
} passwd;
|
||||
|
||||
passwd *getpwuid(uid_t uid);
|
||||
]]
|
||||
|
||||
local ffi_func = function(tbl, id)
|
||||
if tbl[id] then
|
||||
return tbl[id]
|
||||
end
|
||||
local struct = ffi.C.getpwuid(id)
|
||||
local name
|
||||
if struct == nil then
|
||||
name = tostring(id)
|
||||
else
|
||||
name = ffi.string(struct.pw_name)
|
||||
end
|
||||
tbl[id] = name
|
||||
return name
|
||||
end
|
||||
|
||||
local ok = pcall(ffi_func, {}, 1000)
|
||||
if ok then
|
||||
return ffi_func
|
||||
else
|
||||
return fallback
|
||||
end
|
||||
else
|
||||
return fallback
|
||||
end
|
||||
end)()
|
||||
|
||||
local get_groupname = (function()
|
||||
local fallback = function(tbl, id)
|
||||
if not tbl then
|
||||
return id
|
||||
end
|
||||
if tbl[id] then
|
||||
return tbl[id]
|
||||
end
|
||||
tbl[id] = tostring(id)
|
||||
return id
|
||||
end
|
||||
|
||||
if jit and os_sep ~= "\\" then
|
||||
local ffi = require "ffi"
|
||||
ffi.cdef [[
|
||||
typedef unsigned int __gid_t;
|
||||
typedef __gid_t gid_t;
|
||||
|
||||
typedef struct {
|
||||
char *gr_name;
|
||||
char *gr_passwd;
|
||||
__gid_t gr_gid;
|
||||
char **gr_mem;
|
||||
} group;
|
||||
group *getgrgid(gid_t gid);
|
||||
]]
|
||||
|
||||
local ffi_func = function(tbl, id)
|
||||
if tbl[id] then
|
||||
return tbl[id]
|
||||
end
|
||||
local struct = ffi.C.getgrgid(id)
|
||||
local name
|
||||
if struct == nil then
|
||||
name = tostring(id)
|
||||
else
|
||||
name = ffi.string(struct.gr_name)
|
||||
end
|
||||
tbl[id] = name
|
||||
return name
|
||||
end
|
||||
local ok = pcall(ffi_func, {}, 1000)
|
||||
if ok then
|
||||
return ffi_func
|
||||
else
|
||||
return fallback
|
||||
end
|
||||
else
|
||||
return fallback
|
||||
end
|
||||
end)()
|
||||
|
||||
local get_max_len = function(tbl)
|
||||
if not tbl then
|
||||
return 0
|
||||
end
|
||||
local max_len = 0
|
||||
for _, v in pairs(tbl) do
|
||||
if #v > max_len then
|
||||
max_len = #v
|
||||
end
|
||||
end
|
||||
return max_len
|
||||
end
|
||||
|
||||
local gen_ls = function(data, path, opts)
|
||||
if not data or #data == 0 then
|
||||
return {}, {}
|
||||
end
|
||||
|
||||
local check_link = function(per, file)
|
||||
if per:sub(1, 1) == "l" then
|
||||
local resolved = uv.fs_realpath(path .. os_sep .. file)
|
||||
if not resolved then
|
||||
return file
|
||||
end
|
||||
if resolved:sub(1, #path) == path then
|
||||
resolved = resolved:sub(#path + 2, -1)
|
||||
end
|
||||
return string.format("%s -> %s", file, resolved)
|
||||
end
|
||||
return file
|
||||
end
|
||||
|
||||
local results, sections = {}, {}
|
||||
|
||||
local users_tbl = os_sep ~= "\\" and {} or nil
|
||||
local groups_tbl = os_sep ~= "\\" and {} or nil
|
||||
|
||||
local stats, permissions_cache = {}, {}
|
||||
for _, v in ipairs(data) do
|
||||
local stat = uv.fs_lstat(v)
|
||||
if stat then
|
||||
stats[v] = stat
|
||||
get_username(users_tbl, stat.uid)
|
||||
get_groupname(groups_tbl, stat.gid)
|
||||
end
|
||||
end
|
||||
|
||||
local insert_in_results = (function()
|
||||
if not users_tbl and not groups_tbl then
|
||||
local section_spacing_tbl = { [5] = 2, [6] = 0 }
|
||||
|
||||
return function(...)
|
||||
local args = { ... }
|
||||
local section = {
|
||||
{ start_index = 01, end_index = 11 }, -- permissions, hardcoded indexes
|
||||
{ start_index = 12, end_index = 17 }, -- size, hardcoded indexes
|
||||
}
|
||||
local cur_index = 19
|
||||
for k = 5, 6 do
|
||||
local v = section_spacing_tbl[k]
|
||||
local end_index = cur_index + #args[k]
|
||||
table.insert(section, { start_index = cur_index, end_index = end_index })
|
||||
cur_index = end_index + v
|
||||
end
|
||||
table.insert(sections, section)
|
||||
table.insert(
|
||||
results,
|
||||
string.format("%10s %5s %s %s", args[1], args[2], args[5], check_link(args[1], args[6]))
|
||||
)
|
||||
end
|
||||
else
|
||||
local max_user_len = get_max_len(users_tbl)
|
||||
local max_group_len = get_max_len(groups_tbl)
|
||||
|
||||
local section_spacing_tbl = {
|
||||
[3] = { max = max_user_len, add = 1 },
|
||||
[4] = { max = max_group_len, add = 2 },
|
||||
[5] = { add = 2 },
|
||||
[6] = { add = 0 },
|
||||
}
|
||||
local fmt_str = "%10s %5s %-" .. max_user_len .. "s %-" .. max_group_len .. "s %s %s"
|
||||
|
||||
return function(...)
|
||||
local args = { ... }
|
||||
local section = {
|
||||
{ start_index = 01, end_index = 11 }, -- permissions, hardcoded indexes
|
||||
{ start_index = 12, end_index = 17 }, -- size, hardcoded indexes
|
||||
}
|
||||
local cur_index = 18
|
||||
for k = 3, 6 do
|
||||
local v = section_spacing_tbl[k]
|
||||
local end_index = cur_index + #args[k]
|
||||
table.insert(section, { start_index = cur_index, end_index = end_index })
|
||||
if v.max then
|
||||
cur_index = cur_index + v.max + v.add
|
||||
else
|
||||
cur_index = end_index + v.add
|
||||
end
|
||||
end
|
||||
table.insert(sections, section)
|
||||
table.insert(
|
||||
results,
|
||||
string.format(fmt_str, args[1], args[2], args[3], args[4], args[5], check_link(args[1], args[6]))
|
||||
)
|
||||
end
|
||||
end
|
||||
end)()
|
||||
|
||||
for name, stat in pairs(stats) do
|
||||
insert_in_results(
|
||||
gen_permissions(permissions_cache, stat.mode),
|
||||
gen_size(stat.size),
|
||||
get_username(users_tbl, stat.uid),
|
||||
get_groupname(groups_tbl, stat.gid),
|
||||
gen_date(stat.mtime.sec),
|
||||
name:sub(#path + 2, -1)
|
||||
)
|
||||
end
|
||||
|
||||
if opts and opts.group_directories_first then
|
||||
local sorted_results = {}
|
||||
local sorted_sections = {}
|
||||
for k, v in ipairs(results) do
|
||||
if v:sub(1, 1) == "d" then
|
||||
table.insert(sorted_results, v)
|
||||
table.insert(sorted_sections, sections[k])
|
||||
end
|
||||
end
|
||||
for k, v in ipairs(results) do
|
||||
if v:sub(1, 1) ~= "d" then
|
||||
table.insert(sorted_results, v)
|
||||
table.insert(sorted_sections, sections[k])
|
||||
end
|
||||
end
|
||||
return sorted_results, sorted_sections
|
||||
else
|
||||
return results, sections
|
||||
end
|
||||
end
|
||||
|
||||
--- m.ls
|
||||
-- List directory contents. Will always apply --long option. Use scan_dir for without --long
|
||||
-- @param path: string
|
||||
-- string has to be a valid path
|
||||
-- @param opts: table to change behavior
|
||||
-- opts.hidden (bool): if true hidden files will be added
|
||||
-- opts.add_dirs (bool): if true dirs will also be added to the results, default: true
|
||||
-- opts.respect_gitignore (bool): if true will only add files that are not ignored by git
|
||||
-- opts.depth (int): depth on how deep the search should go, default: 1
|
||||
-- opts.group_directories_first (bool): same as real ls
|
||||
-- @return array with formatted output
|
||||
m.ls = function(path, opts)
|
||||
opts = opts or {}
|
||||
opts.depth = opts.depth or 1
|
||||
opts.add_dirs = opts.add_dirs or true
|
||||
local data = m.scan_dir(path, opts)
|
||||
|
||||
return gen_ls(data, path, opts)
|
||||
end
|
||||
|
||||
--- m.ls_async
|
||||
-- List directory contents. Will always apply --long option. Use scan_dir for without --long
|
||||
-- @param path: string
|
||||
-- string has to be a valid path
|
||||
-- @param opts: table to change behavior
|
||||
-- opts.hidden (bool): if true hidden files will be added
|
||||
-- opts.add_dirs (bool): if true dirs will also be added to the results, default: true
|
||||
-- opts.respect_gitignore (bool): if true will only add files that are not ignored by git
|
||||
-- opts.depth (int): depth on how deep the search should go, default: 1
|
||||
-- opts.group_directories_first (bool): same as real ls
|
||||
-- opts.on_exit function(results): will be called at the end (required)
|
||||
m.ls_async = function(path, opts)
|
||||
opts = opts or {}
|
||||
opts.depth = opts.depth or 1
|
||||
opts.add_dirs = opts.add_dirs or true
|
||||
|
||||
local opts_copy = vim.deepcopy(opts)
|
||||
|
||||
opts_copy.on_exit = function(data)
|
||||
if opts.on_exit then
|
||||
opts.on_exit(gen_ls(data, path, opts_copy))
|
||||
end
|
||||
end
|
||||
|
||||
m.scan_dir_async(path, opts_copy)
|
||||
end
|
||||
|
||||
return m
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
local path = require("plenary.path").path
|
||||
|
||||
local M = {}
|
||||
|
||||
M.strdisplaywidth = (function()
|
||||
local fallback = function(str, col)
|
||||
str = tostring(str)
|
||||
if vim.in_fast_event() then
|
||||
return #str - (col or 0)
|
||||
end
|
||||
return vim.fn.strdisplaywidth(str, col)
|
||||
end
|
||||
|
||||
if jit and path.sep ~= [[\]] then
|
||||
local ffi = require "ffi"
|
||||
ffi.cdef [[
|
||||
typedef unsigned char char_u;
|
||||
int linetabsize_col(int startcol, char_u *s);
|
||||
]]
|
||||
|
||||
local ffi_func = function(str, col)
|
||||
str = tostring(str)
|
||||
local startcol = col or 0
|
||||
local s = ffi.new("char[?]", #str + 1)
|
||||
ffi.copy(s, str)
|
||||
return ffi.C.linetabsize_col(startcol, s) - startcol
|
||||
end
|
||||
|
||||
local ok = pcall(ffi_func, "hello")
|
||||
if ok then
|
||||
return ffi_func
|
||||
else
|
||||
return fallback
|
||||
end
|
||||
else
|
||||
return fallback
|
||||
end
|
||||
end)()
|
||||
|
||||
M.strcharpart = (function()
|
||||
local fallback = function(str, nchar, charlen)
|
||||
if vim.in_fast_event() then
|
||||
return str:sub(nchar + 1, charlen)
|
||||
end
|
||||
return vim.fn.strcharpart(str, nchar, charlen)
|
||||
end
|
||||
|
||||
if jit and path.sep ~= [[\]] then
|
||||
local ffi = require "ffi"
|
||||
ffi.cdef [[
|
||||
typedef unsigned char char_u;
|
||||
int utf_ptr2len(const char_u *const p);
|
||||
]]
|
||||
|
||||
local function utf_ptr2len(str)
|
||||
local c_str = ffi.new("char[?]", #str + 1)
|
||||
ffi.copy(c_str, str)
|
||||
return ffi.C.utf_ptr2len(c_str)
|
||||
end
|
||||
|
||||
local ok = pcall(utf_ptr2len, "🔭")
|
||||
if not ok then
|
||||
return fallback
|
||||
end
|
||||
|
||||
return function(str, nchar, charlen)
|
||||
local nbyte = 0
|
||||
if nchar > 0 then
|
||||
while nchar > 0 and nbyte < #str do
|
||||
nbyte = nbyte + utf_ptr2len(str:sub(nbyte + 1))
|
||||
nchar = nchar - 1
|
||||
end
|
||||
else
|
||||
nbyte = nchar
|
||||
end
|
||||
|
||||
local len = 0
|
||||
if charlen then
|
||||
while charlen > 0 and nbyte + len < #str do
|
||||
local off = nbyte + len
|
||||
if off < 0 then
|
||||
len = len + 1
|
||||
else
|
||||
len = len + utf_ptr2len(str:sub(off + 1))
|
||||
end
|
||||
charlen = charlen - 1
|
||||
end
|
||||
else
|
||||
len = #str - nbyte
|
||||
end
|
||||
|
||||
if nbyte < 0 then
|
||||
len = len + nbyte
|
||||
nbyte = 0
|
||||
elseif nbyte > #str then
|
||||
nbyte = #str
|
||||
end
|
||||
if len < 0 then
|
||||
len = 0
|
||||
elseif nbyte + len > #str then
|
||||
len = #str - nbyte
|
||||
end
|
||||
|
||||
return str:sub(nbyte + 1, nbyte + len)
|
||||
end
|
||||
else
|
||||
return fallback
|
||||
end
|
||||
end)()
|
||||
|
||||
local truncate = function(str, len, dots, direction)
|
||||
if M.strdisplaywidth(str) <= len then
|
||||
return str
|
||||
end
|
||||
local start = direction > 0 and 0 or str:len()
|
||||
local current = 0
|
||||
local result = ""
|
||||
local len_of_dots = M.strdisplaywidth(dots)
|
||||
local concat = function(a, b, dir)
|
||||
if dir > 0 then
|
||||
return a .. b
|
||||
else
|
||||
return b .. a
|
||||
end
|
||||
end
|
||||
while true do
|
||||
local part = M.strcharpart(str, start, 1)
|
||||
current = current + M.strdisplaywidth(part)
|
||||
if (current + len_of_dots) > len then
|
||||
result = concat(result, dots, direction)
|
||||
break
|
||||
end
|
||||
result = concat(result, part, direction)
|
||||
start = start + direction
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
M.truncate = function(str, len, dots, direction)
|
||||
str = tostring(str) -- We need to make sure its an actually a string and not a number
|
||||
dots = dots or "…"
|
||||
direction = direction or 1
|
||||
if direction ~= 0 then
|
||||
return truncate(str, len, dots, direction)
|
||||
else
|
||||
if M.strdisplaywidth(str) <= len then
|
||||
return str
|
||||
end
|
||||
local len1 = math.floor((len + M.strdisplaywidth(dots)) / 2)
|
||||
local s1 = truncate(str, len1, dots, 1)
|
||||
local len2 = len - M.strdisplaywidth(s1) + M.strdisplaywidth(dots)
|
||||
local s2 = truncate(str, len2, dots, -1)
|
||||
return s1 .. s2:sub(dots:len() + 1)
|
||||
end
|
||||
end
|
||||
|
||||
M.align_str = function(string, width, right_justify)
|
||||
local str_len = M.strdisplaywidth(string)
|
||||
return right_justify and string.rep(" ", width - str_len) .. string or string .. string.rep(" ", width - str_len)
|
||||
end
|
||||
|
||||
M.dedent = function(str, leave_indent)
|
||||
-- Check each line and detect the minimum indent.
|
||||
local indent
|
||||
local info = {}
|
||||
for line in str:gmatch "[^\n]*\n?" do
|
||||
-- It matches '' for the last line.
|
||||
if line ~= "" then
|
||||
local chars, width
|
||||
local line_indent = line:match "^[ \t]+"
|
||||
if line_indent then
|
||||
chars = #line_indent
|
||||
width = M.strdisplaywidth(line_indent)
|
||||
if not indent or width < indent then
|
||||
indent = width
|
||||
end
|
||||
-- Ignore empty lines
|
||||
elseif line ~= "\n" then
|
||||
indent = 0
|
||||
end
|
||||
table.insert(info, { line = line, chars = chars, width = width })
|
||||
end
|
||||
end
|
||||
|
||||
-- Build up the result
|
||||
leave_indent = leave_indent or 0
|
||||
local result = {}
|
||||
for _, i in ipairs(info) do
|
||||
local line
|
||||
if i.chars then
|
||||
local content = i.line:sub(i.chars + 1)
|
||||
local indent_width = i.width - indent + leave_indent
|
||||
line = (" "):rep(indent_width) .. content
|
||||
elseif i.line == "\n" then
|
||||
line = "\n"
|
||||
else
|
||||
line = (" "):rep(leave_indent) .. i.line
|
||||
end
|
||||
table.insert(result, line)
|
||||
end
|
||||
return table.concat(result)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
local tbl = {}
|
||||
|
||||
function tbl.apply_defaults(original, defaults)
|
||||
if original == nil then
|
||||
original = {}
|
||||
end
|
||||
|
||||
original = vim.deepcopy(original)
|
||||
|
||||
for k, v in pairs(defaults) do
|
||||
if original[k] == nil then
|
||||
original[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
return original
|
||||
end
|
||||
|
||||
function tbl.pack(...)
|
||||
return { n = select("#", ...), ... }
|
||||
end
|
||||
|
||||
function tbl.unpack(t, i, j)
|
||||
return unpack(t, i or 1, j or t.n or #t)
|
||||
end
|
||||
|
||||
---Freeze a table. A frozen table is not able to be modified.
|
||||
---http://lua-users.org/wiki/ReadOnlyTables
|
||||
---@param t table
|
||||
---@return table
|
||||
function tbl.freeze(t)
|
||||
return setmetatable({}, {
|
||||
__index = t,
|
||||
__newindex = function()
|
||||
error "Attempt to modify frozen table"
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return tbl
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
local Path = require "plenary.path"
|
||||
local Job = require "plenary.job"
|
||||
|
||||
local f = require "plenary.functional"
|
||||
local log = require "plenary.log"
|
||||
local win_float = require "plenary.window.float"
|
||||
|
||||
local headless = require("plenary.nvim_meta").is_headless
|
||||
|
||||
local plenary_dir = vim.fn.fnamemodify(debug.getinfo(1).source:match "@?(.*[/\\])", ":p:h:h:h")
|
||||
|
||||
local harness = {}
|
||||
|
||||
local print_output = vim.schedule_wrap(function(_, ...)
|
||||
for _, v in ipairs { ... } do
|
||||
io.stdout:write(tostring(v))
|
||||
io.stdout:write "\n"
|
||||
end
|
||||
|
||||
vim.cmd [[mode]]
|
||||
end)
|
||||
|
||||
local get_nvim_output = function(job_id)
|
||||
return vim.schedule_wrap(function(bufnr, ...)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
for _, v in ipairs { ... } do
|
||||
vim.api.nvim_chan_send(job_id, v .. "\r\n")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function harness.test_directory_command(command)
|
||||
local split_string = vim.split(command, " ")
|
||||
local directory = vim.fn.expand(table.remove(split_string, 1))
|
||||
|
||||
local opts = assert(loadstring("return " .. table.concat(split_string, " ")))()
|
||||
|
||||
return harness.test_directory(directory, opts)
|
||||
end
|
||||
|
||||
local function test_paths(paths, opts)
|
||||
local minimal = not opts or not opts.init or opts.minimal or opts.minimal_init
|
||||
|
||||
opts = vim.tbl_deep_extend("force", {
|
||||
nvim_cmd = vim.v.progpath,
|
||||
winopts = { winblend = 3 },
|
||||
sequential = false,
|
||||
keep_going = true,
|
||||
timeout = 50000,
|
||||
}, opts or {})
|
||||
|
||||
vim.env.PLENARY_TEST_TIMEOUT = opts.timeout
|
||||
|
||||
local res = {}
|
||||
if not headless then
|
||||
res = win_float.percentage_range_window(0.95, 0.70, opts.winopts)
|
||||
|
||||
res.job_id = vim.api.nvim_open_term(res.bufnr, {})
|
||||
vim.api.nvim_buf_set_keymap(res.bufnr, "n", "q", ":q<CR>", {})
|
||||
|
||||
vim.api.nvim_win_set_option(res.win_id, "winhl", "Normal:Normal")
|
||||
vim.api.nvim_win_set_option(res.win_id, "conceallevel", 3)
|
||||
vim.api.nvim_win_set_option(res.win_id, "concealcursor", "n")
|
||||
|
||||
if res.border_win_id then
|
||||
vim.api.nvim_win_set_option(res.border_win_id, "winhl", "Normal:Normal")
|
||||
end
|
||||
|
||||
if res.bufnr then
|
||||
vim.api.nvim_buf_set_option(res.bufnr, "filetype", "PlenaryTestPopup")
|
||||
end
|
||||
vim.cmd "mode"
|
||||
end
|
||||
|
||||
local outputter = headless and print_output or get_nvim_output(res.job_id)
|
||||
|
||||
local path_len = #paths
|
||||
local failure = false
|
||||
|
||||
local jobs = vim.tbl_map(function(p)
|
||||
local args = {
|
||||
"--headless",
|
||||
"-c",
|
||||
"set rtp+=.," .. vim.fn.escape(plenary_dir, " ") .. " | runtime plugin/plenary.vim",
|
||||
}
|
||||
|
||||
if minimal then
|
||||
table.insert(args, "--noplugin")
|
||||
if opts.minimal_init then
|
||||
table.insert(args, "-u")
|
||||
table.insert(args, opts.minimal_init)
|
||||
end
|
||||
elseif opts.init ~= nil then
|
||||
table.insert(args, "-u")
|
||||
table.insert(args, opts.init)
|
||||
end
|
||||
|
||||
table.insert(args, "-c")
|
||||
table.insert(args, string.format('lua require("plenary.busted").run("%s")', p:absolute():gsub("\\", "\\\\")))
|
||||
|
||||
local job = Job:new {
|
||||
command = opts.nvim_cmd,
|
||||
args = args,
|
||||
|
||||
-- Can be turned on to debug
|
||||
on_stdout = function(_, data)
|
||||
if path_len == 1 then
|
||||
outputter(res.bufnr, data)
|
||||
end
|
||||
end,
|
||||
|
||||
on_stderr = function(_, data)
|
||||
if path_len == 1 then
|
||||
outputter(res.bufnr, data)
|
||||
end
|
||||
end,
|
||||
|
||||
on_exit = vim.schedule_wrap(function(j_self, _, _)
|
||||
if path_len ~= 1 then
|
||||
outputter(res.bufnr, unpack(j_self:stderr_result()))
|
||||
outputter(res.bufnr, unpack(j_self:result()))
|
||||
end
|
||||
|
||||
vim.cmd "mode"
|
||||
end),
|
||||
}
|
||||
job.nvim_busted_path = p.filename
|
||||
return job
|
||||
end, paths)
|
||||
|
||||
log.debug "Running..."
|
||||
for i, j in ipairs(jobs) do
|
||||
outputter(res.bufnr, "Scheduling: " .. j.nvim_busted_path)
|
||||
j:start()
|
||||
if opts.sequential then
|
||||
log.debug("... Sequential wait for job number", i)
|
||||
if not Job.join(j, opts.timeout) then
|
||||
log.debug("... Timed out job number", i)
|
||||
failure = true
|
||||
pcall(function()
|
||||
j.handle:kill(15) -- SIGTERM
|
||||
end)
|
||||
else
|
||||
log.debug("... Completed job number", i, j.code, j.signal)
|
||||
failure = failure or j.code ~= 0 or j.signal ~= 0
|
||||
end
|
||||
if failure and not opts.keep_going then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: Probably want to let people know when we've completed everything.
|
||||
if not headless then
|
||||
return
|
||||
end
|
||||
|
||||
if not opts.sequential then
|
||||
table.insert(jobs, opts.timeout)
|
||||
log.debug "... Parallel wait"
|
||||
Job.join(unpack(jobs))
|
||||
log.debug "... Completed jobs"
|
||||
table.remove(jobs, table.getn(jobs))
|
||||
failure = f.any(function(_, v)
|
||||
return v.code ~= 0
|
||||
end, jobs)
|
||||
end
|
||||
vim.wait(100)
|
||||
|
||||
if headless then
|
||||
if failure then
|
||||
return vim.cmd "1cq"
|
||||
end
|
||||
|
||||
return vim.cmd "0cq"
|
||||
end
|
||||
end
|
||||
|
||||
function harness.test_directory(directory, opts)
|
||||
print "Starting..."
|
||||
directory = directory:gsub("\\", "/")
|
||||
local paths = harness._find_files_to_run(directory)
|
||||
|
||||
-- Paths work strangely on Windows, so lets have abs paths
|
||||
if vim.fn.has "win32" == 1 or vim.fn.has "win64" == 1 then
|
||||
paths = vim.tbl_map(function(p)
|
||||
return Path:new(directory, p.filename)
|
||||
end, paths)
|
||||
end
|
||||
|
||||
test_paths(paths, opts)
|
||||
end
|
||||
|
||||
function harness.test_file(filepath)
|
||||
test_paths { Path:new(filepath) }
|
||||
end
|
||||
|
||||
function harness._find_files_to_run(directory)
|
||||
local finder
|
||||
if vim.fn.has "win32" == 1 or vim.fn.has "win64" == 1 then
|
||||
-- On windows use powershell Get-ChildItem instead
|
||||
local cmd = vim.fn.executable "pwsh.exe" == 1 and "pwsh" or "powershell"
|
||||
finder = Job:new {
|
||||
command = cmd,
|
||||
args = { "-NoProfile", "-Command", [[Get-ChildItem -Recurse -n -Filter "*_spec.lua"]] },
|
||||
cwd = directory,
|
||||
}
|
||||
else
|
||||
-- everywhere else use find
|
||||
finder = Job:new {
|
||||
command = "find",
|
||||
args = { directory, "-type", "f", "-name", "*_spec.lua" },
|
||||
}
|
||||
end
|
||||
|
||||
return vim.tbl_map(Path.new, finder:sync(vim.env.PLENARY_TEST_TIMEOUT))
|
||||
end
|
||||
|
||||
function harness._run_path(test_type, directory)
|
||||
local paths = harness._find_files_to_run(directory)
|
||||
|
||||
local bufnr = 0
|
||||
local win_id = 0
|
||||
|
||||
for _, p in pairs(paths) do
|
||||
print " "
|
||||
print("Loading Tests For: ", p:absolute(), "\n")
|
||||
|
||||
local ok, _ = pcall(function()
|
||||
dofile(p:absolute())
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
print "Failed to load file"
|
||||
end
|
||||
end
|
||||
|
||||
harness:run(test_type, bufnr, win_id)
|
||||
vim.cmd "qa!"
|
||||
|
||||
return paths
|
||||
end
|
||||
|
||||
return harness
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
return {
|
||||
rotate = require "plenary.vararg.rotate",
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
---@brief [[
|
||||
---Do not edit this file, it was generated!
|
||||
---Provides a function to rotate a lua vararg
|
||||
---@brief ]]
|
||||
local tbl = require "plenary.tbl"
|
||||
|
||||
local rotate_lookup = {}
|
||||
|
||||
rotate_lookup[1] = function(A0)
|
||||
return A0
|
||||
end
|
||||
|
||||
rotate_lookup[2] = function(A0, A1)
|
||||
return A1, A0
|
||||
end
|
||||
|
||||
rotate_lookup[3] = function(A0, A1, A2)
|
||||
return A1, A2, A0
|
||||
end
|
||||
|
||||
rotate_lookup[4] = function(A0, A1, A2, A3)
|
||||
return A1, A2, A3, A0
|
||||
end
|
||||
|
||||
rotate_lookup[5] = function(A0, A1, A2, A3, A4)
|
||||
return A1, A2, A3, A4, A0
|
||||
end
|
||||
|
||||
rotate_lookup[6] = function(A0, A1, A2, A3, A4, A5)
|
||||
return A1, A2, A3, A4, A5, A0
|
||||
end
|
||||
|
||||
rotate_lookup[7] = function(A0, A1, A2, A3, A4, A5, A6)
|
||||
return A1, A2, A3, A4, A5, A6, A0
|
||||
end
|
||||
|
||||
rotate_lookup[8] = function(A0, A1, A2, A3, A4, A5, A6, A7)
|
||||
return A1, A2, A3, A4, A5, A6, A7, A0
|
||||
end
|
||||
|
||||
rotate_lookup[9] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8)
|
||||
return A1, A2, A3, A4, A5, A6, A7, A8, A0
|
||||
end
|
||||
|
||||
rotate_lookup[10] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9)
|
||||
return A1, A2, A3, A4, A5, A6, A7, A8, A9, A0
|
||||
end
|
||||
|
||||
rotate_lookup[11] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10)
|
||||
return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A0
|
||||
end
|
||||
|
||||
rotate_lookup[12] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11)
|
||||
return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A0
|
||||
end
|
||||
|
||||
rotate_lookup[13] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12)
|
||||
return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A0
|
||||
end
|
||||
|
||||
rotate_lookup[14] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13)
|
||||
return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A0
|
||||
end
|
||||
|
||||
rotate_lookup[15] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14)
|
||||
return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A0
|
||||
end
|
||||
|
||||
local function rotate_n(first, ...)
|
||||
local n = select("#", ...) + 1
|
||||
local args = tbl.pack(...)
|
||||
args[n] = first
|
||||
return tbl.unpack(args, 1, n)
|
||||
end
|
||||
|
||||
local function rotate(nargs, ...)
|
||||
if nargs == nil or nargs < 1 then
|
||||
return
|
||||
end
|
||||
return (rotate_lookup[nargs] or rotate_n)(...)
|
||||
end
|
||||
|
||||
return rotate
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
local strings = require "plenary.strings"
|
||||
|
||||
local Border = {}
|
||||
|
||||
Border.__index = Border
|
||||
|
||||
Border._default_thickness = {
|
||||
top = 1,
|
||||
right = 1,
|
||||
bot = 1,
|
||||
left = 1,
|
||||
}
|
||||
|
||||
local calc_left_start = function(title_pos, title_len, total_width)
|
||||
if string.find(title_pos, "W") then
|
||||
return 0
|
||||
elseif string.find(title_pos, "E") then
|
||||
return total_width - title_len
|
||||
else
|
||||
return math.floor((total_width - title_len) / 2)
|
||||
end
|
||||
end
|
||||
|
||||
local create_horizontal_line = function(title, pos, width, left_char, mid_char, right_char)
|
||||
local title_len
|
||||
if title == "" then
|
||||
title_len = 0
|
||||
else
|
||||
local len = strings.strdisplaywidth(title)
|
||||
local max_title_width = width - 2
|
||||
if len > max_title_width then
|
||||
title = strings.truncate(title, max_title_width)
|
||||
len = strings.strdisplaywidth(title)
|
||||
end
|
||||
title = string.format(" %s ", title)
|
||||
title_len = len + 2
|
||||
end
|
||||
|
||||
local left_start = calc_left_start(pos, title_len, width)
|
||||
|
||||
local horizontal_line = string.format(
|
||||
"%s%s%s%s%s",
|
||||
left_char,
|
||||
string.rep(mid_char, left_start),
|
||||
title,
|
||||
string.rep(mid_char, width - title_len - left_start),
|
||||
right_char
|
||||
)
|
||||
local ranges = {}
|
||||
if title_len ~= 0 then
|
||||
-- Need to calculate again due to multi-byte characters
|
||||
local r_start = string.len(left_char) + math.max(left_start, 0) * string.len(mid_char)
|
||||
ranges = { { r_start, r_start + string.len(title) } }
|
||||
end
|
||||
return horizontal_line, ranges
|
||||
end
|
||||
|
||||
function Border._create_lines(content_win_id, content_win_options, border_win_options)
|
||||
local content_pos = vim.api.nvim_win_get_position(content_win_id)
|
||||
local content_height = vim.api.nvim_win_get_height(content_win_id)
|
||||
local content_width = vim.api.nvim_win_get_width(content_win_id)
|
||||
|
||||
-- TODO: Handle border width, which I haven't right here.
|
||||
local thickness = border_win_options.border_thickness
|
||||
|
||||
local top_enabled = thickness.top == 1
|
||||
local right_enabled = thickness.right == 1 and content_pos[2] + content_width < vim.o.columns
|
||||
local bot_enabled = thickness.bot == 1
|
||||
local left_enabled = thickness.left == 1 and content_pos[2] > 0
|
||||
|
||||
border_win_options.border_thickness.left = left_enabled and 1 or 0
|
||||
border_win_options.border_thickness.right = right_enabled and 1 or 0
|
||||
|
||||
local border_lines = {}
|
||||
local ranges = {}
|
||||
|
||||
-- border_win_options.title should have be a list with entries of the
|
||||
-- form: { pos = foo, text = bar }.
|
||||
-- pos can take values in { "NW", "N", "NE", "SW", "S", "SE" }
|
||||
local titles = type(border_win_options.title) == "string" and { { pos = "N", text = border_win_options.title } }
|
||||
or border_win_options.title
|
||||
or {}
|
||||
|
||||
local topline = nil
|
||||
local topleft = (left_enabled and border_win_options.topleft) or ""
|
||||
local topright = (right_enabled and border_win_options.topright) or ""
|
||||
-- Only calculate the topline if there is space above the first content row (relative to the editor)
|
||||
if content_pos[1] > 0 then
|
||||
for _, title in ipairs(titles) do
|
||||
if string.find(title.pos, "N") then
|
||||
local top_ranges
|
||||
topline, top_ranges = create_horizontal_line(
|
||||
title.text,
|
||||
title.pos,
|
||||
content_win_options.width,
|
||||
topleft,
|
||||
border_win_options.top or "",
|
||||
topright
|
||||
)
|
||||
for _, r in pairs(top_ranges) do
|
||||
table.insert(ranges, { 0, r[1], r[2] })
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
if topline == nil then
|
||||
if top_enabled then
|
||||
topline = topleft .. string.rep(border_win_options.top, content_win_options.width) .. topright
|
||||
end
|
||||
end
|
||||
else
|
||||
border_win_options.border_thickness.top = 0
|
||||
end
|
||||
|
||||
if topline then
|
||||
table.insert(border_lines, topline)
|
||||
end
|
||||
|
||||
local middle_line = string.format(
|
||||
"%s%s%s",
|
||||
(left_enabled and border_win_options.left) or "",
|
||||
string.rep(" ", content_win_options.width),
|
||||
(right_enabled and border_win_options.right) or ""
|
||||
)
|
||||
|
||||
for _ = 1, content_win_options.height do
|
||||
table.insert(border_lines, middle_line)
|
||||
end
|
||||
|
||||
local botline = nil
|
||||
local botleft = (left_enabled and border_win_options.botleft) or ""
|
||||
local botright = (right_enabled and border_win_options.botright) or ""
|
||||
if content_pos[1] + content_height < vim.o.lines then
|
||||
for _, title in ipairs(titles) do
|
||||
if string.find(title.pos, "S") then
|
||||
local bot_ranges
|
||||
botline, bot_ranges = create_horizontal_line(
|
||||
title.text,
|
||||
title.pos,
|
||||
content_win_options.width,
|
||||
botleft,
|
||||
border_win_options.bot or "",
|
||||
botright
|
||||
)
|
||||
for _, r in pairs(bot_ranges) do
|
||||
table.insert(ranges, { content_win_options.height + thickness.top, r[1], r[2] })
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
if botline == nil then
|
||||
if bot_enabled then
|
||||
botline = botleft .. string.rep(border_win_options.bot, content_win_options.width) .. botright
|
||||
end
|
||||
end
|
||||
else
|
||||
border_win_options.border_thickness.bot = 0
|
||||
end
|
||||
|
||||
if botline then
|
||||
table.insert(border_lines, botline)
|
||||
end
|
||||
|
||||
return border_lines, ranges
|
||||
end
|
||||
|
||||
local set_title_highlights = function(bufnr, ranges, hl)
|
||||
-- Check if both `hl` and `ranges` are provided, and `ranges` is not the empty table.
|
||||
if hl and ranges and next(ranges) then
|
||||
for _, r in pairs(ranges) do
|
||||
vim.api.nvim_buf_add_highlight(bufnr, -1, hl, r[1], r[2], r[3])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Border:change_title(new_title, pos)
|
||||
if self._border_win_options.title == new_title then
|
||||
return
|
||||
end
|
||||
|
||||
pos = pos
|
||||
or (self._border_win_options.title and self._border_win_options.title[1] and self._border_win_options.title[1].pos)
|
||||
if pos == nil then
|
||||
self._border_win_options.title = new_title
|
||||
else
|
||||
self._border_win_options.title = { { text = new_title, pos = pos } }
|
||||
end
|
||||
|
||||
self.contents, self.title_ranges =
|
||||
Border._create_lines(self.content_win_id, self.content_win_options, self._border_win_options)
|
||||
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents)
|
||||
|
||||
set_title_highlights(self.bufnr, self.title_ranges, self._border_win_options.titlehighlight)
|
||||
end
|
||||
|
||||
-- Updates characters for border lines, and returns nvim_win_config
|
||||
-- (generally used in conjunction with `move` or `new`)
|
||||
function Border:__align_calc_config(content_win_options, border_win_options)
|
||||
border_win_options = vim.tbl_deep_extend("keep", border_win_options, {
|
||||
border_thickness = Border._default_thickness,
|
||||
|
||||
-- Border options, could be passed as a list?
|
||||
topleft = "╔",
|
||||
topright = "╗",
|
||||
top = "═",
|
||||
left = "║",
|
||||
right = "║",
|
||||
botleft = "╚",
|
||||
botright = "╝",
|
||||
bot = "═",
|
||||
})
|
||||
|
||||
-- Ensure the relevant contents and border win_options are set
|
||||
self._border_win_options = border_win_options
|
||||
self.content_win_options = content_win_options
|
||||
-- Update border characters and title_ranges
|
||||
self.contents, self.title_ranges = Border._create_lines(self.content_win_id, content_win_options, border_win_options)
|
||||
|
||||
vim.api.nvim_buf_set_option(self.bufnr, "modifiable", true)
|
||||
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents)
|
||||
|
||||
local thickness = border_win_options.border_thickness
|
||||
local nvim_win_config = {
|
||||
anchor = content_win_options.anchor,
|
||||
relative = content_win_options.relative,
|
||||
style = "minimal",
|
||||
row = content_win_options.row - thickness.top,
|
||||
col = content_win_options.col - thickness.left,
|
||||
width = content_win_options.width + thickness.left + thickness.right,
|
||||
height = content_win_options.height + thickness.top + thickness.bot,
|
||||
zindex = content_win_options.zindex or 50,
|
||||
noautocmd = content_win_options.noautocmd,
|
||||
focusable = vim.F.if_nil(border_win_options.focusable, false),
|
||||
border = "none",
|
||||
}
|
||||
|
||||
return nvim_win_config
|
||||
end
|
||||
|
||||
-- Sets the size and position of the given Border.
|
||||
-- Can be used to create a new window (with `create_window = true`)
|
||||
-- or change an existing one
|
||||
function Border:move(content_win_options, border_win_options)
|
||||
-- Update lines in border buffer, and get config for border window
|
||||
local nvim_win_config = self:__align_calc_config(content_win_options, border_win_options)
|
||||
|
||||
-- Set config for border window
|
||||
vim.api.nvim_win_set_config(self.win_id, nvim_win_config)
|
||||
|
||||
set_title_highlights(self.bufnr, self.title_ranges, self._border_win_options.titlehighlight)
|
||||
end
|
||||
|
||||
function Border:new(content_bufnr, content_win_id, content_win_options, border_win_options)
|
||||
assert(type(content_win_id) == "number", "Must supply a valid win_id. It's possible you forgot to call with ':'")
|
||||
|
||||
local obj = {}
|
||||
|
||||
obj.content_win_id = content_win_id
|
||||
|
||||
obj.bufnr = vim.api.nvim_create_buf(false, true)
|
||||
assert(obj.bufnr, "Failed to create border buffer")
|
||||
vim.api.nvim_buf_set_option(obj.bufnr, "bufhidden", "wipe")
|
||||
|
||||
-- Create a border window and buffer, with border characters around the edge
|
||||
local nvim_win_config = Border.__align_calc_config(obj, content_win_options, border_win_options)
|
||||
obj.win_id = vim.api.nvim_open_win(obj.bufnr, false, nvim_win_config)
|
||||
|
||||
if border_win_options.highlight then
|
||||
vim.api.nvim_win_set_option(obj.win_id, "winhl", border_win_options.highlight)
|
||||
end
|
||||
|
||||
set_title_highlights(obj.bufnr, obj.title_ranges, obj._border_win_options.titlehighlight)
|
||||
|
||||
vim.cmd(
|
||||
string.format(
|
||||
"autocmd BufDelete <buffer=%s> ++nested ++once :lua require('plenary.window').close_related_win(%s, %s)",
|
||||
content_bufnr,
|
||||
content_win_id,
|
||||
obj.win_id
|
||||
)
|
||||
)
|
||||
|
||||
vim.cmd(
|
||||
string.format(
|
||||
"autocmd WinClosed <buffer=%s> ++nested ++once :lua require('plenary.window').try_close(%s, true)",
|
||||
content_bufnr,
|
||||
obj.win_id
|
||||
)
|
||||
)
|
||||
|
||||
setmetatable(obj, Border)
|
||||
|
||||
return obj
|
||||
end
|
||||
|
||||
return Border
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
local Border = require "plenary.window.border"
|
||||
local tbl = require "plenary.tbl"
|
||||
|
||||
_AssociatedBufs = {}
|
||||
|
||||
local clear_buf_on_leave = function(bufnr)
|
||||
vim.cmd(
|
||||
string.format(
|
||||
"autocmd WinLeave,BufLeave,BufDelete <buffer=%s> ++once ++nested lua require('plenary.window.float').clear(%s)",
|
||||
bufnr,
|
||||
bufnr
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
local win_float = {}
|
||||
|
||||
win_float.default_options = {
|
||||
winblend = 15,
|
||||
percentage = 0.9,
|
||||
}
|
||||
|
||||
function win_float.default_opts(options)
|
||||
options = tbl.apply_defaults(options, win_float.default_options)
|
||||
|
||||
local width = math.floor(vim.o.columns * options.percentage)
|
||||
local height = math.floor(vim.o.lines * options.percentage)
|
||||
|
||||
local top = math.floor(((vim.o.lines - height) / 2) - 1)
|
||||
local left = math.floor((vim.o.columns - width) / 2)
|
||||
|
||||
local opts = {
|
||||
relative = "editor",
|
||||
row = top,
|
||||
col = left,
|
||||
width = width,
|
||||
height = height,
|
||||
style = "minimal",
|
||||
}
|
||||
|
||||
return opts
|
||||
end
|
||||
|
||||
function win_float.centered(options)
|
||||
options = tbl.apply_defaults(options, win_float.default_options)
|
||||
|
||||
local win_opts = win_float.default_opts(options)
|
||||
|
||||
local bufnr = options.bufnr or vim.api.nvim_create_buf(false, true)
|
||||
local win_id = vim.api.nvim_open_win(bufnr, true, win_opts)
|
||||
|
||||
vim.cmd "setlocal nocursorcolumn"
|
||||
vim.api.nvim_win_set_option(win_id, "winblend", options.winblend)
|
||||
|
||||
vim.cmd(string.format("autocmd WinLeave <buffer> silent! execute 'bdelete! %s'", bufnr))
|
||||
|
||||
return {
|
||||
bufnr = bufnr,
|
||||
win_id = win_id,
|
||||
}
|
||||
end
|
||||
|
||||
function win_float.centered_with_top_win(top_text, options)
|
||||
options = tbl.apply_defaults(options, win_float.default_options)
|
||||
|
||||
table.insert(top_text, 1, string.rep("=", 80))
|
||||
table.insert(top_text, string.rep("=", 80))
|
||||
|
||||
local primary_win_opts = win_float.default_opts(nil, nil, options)
|
||||
local minor_win_opts = vim.deepcopy(primary_win_opts)
|
||||
|
||||
primary_win_opts.height = primary_win_opts.height - #top_text - 1
|
||||
primary_win_opts.row = primary_win_opts.row + #top_text + 1
|
||||
|
||||
minor_win_opts.height = #top_text
|
||||
|
||||
local minor_bufnr = vim.api.nvim_create_buf(false, true)
|
||||
local minor_win_id = vim.api.nvim_open_win(minor_bufnr, true, minor_win_opts)
|
||||
|
||||
vim.cmd "setlocal nocursorcolumn"
|
||||
vim.api.nvim_win_set_option(minor_win_id, "winblend", options.winblend)
|
||||
|
||||
vim.api.nvim_buf_set_lines(minor_bufnr, 0, -1, false, top_text)
|
||||
|
||||
local primary_bufnr = vim.api.nvim_create_buf(false, true)
|
||||
local primary_win_id = vim.api.nvim_open_win(primary_bufnr, true, primary_win_opts)
|
||||
|
||||
vim.cmd "setlocal nocursorcolumn"
|
||||
vim.api.nvim_win_set_option(primary_win_id, "winblend", options.winblend)
|
||||
|
||||
-- vim.cmd(
|
||||
-- string.format(
|
||||
-- "autocmd WinLeave,BufDelete,BufLeave <buffer=%s> ++once ++nested silent! execute 'bdelete! %s'",
|
||||
-- primary_buf,
|
||||
-- minor_buf
|
||||
-- )
|
||||
-- )
|
||||
|
||||
-- vim.cmd(
|
||||
-- string.format(
|
||||
-- "autocmd WinLeave,BufDelete,BufLeave <buffer> ++once ++nested silent! execute 'bdelete! %s'",
|
||||
-- primary_buf
|
||||
-- )
|
||||
-- )
|
||||
|
||||
local primary_border = Border:new(primary_bufnr, primary_win_id, primary_win_opts, {})
|
||||
local minor_border = Border:new(minor_bufnr, minor_win_id, minor_win_opts, {})
|
||||
|
||||
_AssociatedBufs[primary_bufnr] = {
|
||||
primary_win_id,
|
||||
minor_win_id,
|
||||
primary_border.win_id,
|
||||
minor_border.win_id,
|
||||
}
|
||||
|
||||
clear_buf_on_leave(primary_bufnr)
|
||||
|
||||
return {
|
||||
bufnr = primary_bufnr,
|
||||
win_id = primary_win_id,
|
||||
|
||||
minor_bufnr = minor_bufnr,
|
||||
minor_win_id = minor_win_id,
|
||||
}
|
||||
end
|
||||
|
||||
--- Create window that takes up certain percentags of the current screen.
|
||||
---
|
||||
--- Works regardless of current buffers, tabs, splits, etc.
|
||||
--@param col_range number | Table:
|
||||
-- If number, then center the window taking up this percentage of the screen.
|
||||
-- If table, first index should be start, second_index should be end
|
||||
--@param row_range number | Table:
|
||||
-- If number, then center the window taking up this percentage of the screen.
|
||||
-- If table, first index should be start, second_index should be end
|
||||
--@param win_opts Table
|
||||
--@param border_opts Table
|
||||
function win_float.percentage_range_window(col_range, row_range, win_opts, border_opts)
|
||||
win_opts = tbl.apply_defaults(win_opts, win_float.default_options)
|
||||
|
||||
local default_win_opts = win_float.default_opts(win_opts)
|
||||
default_win_opts.relative = "editor"
|
||||
|
||||
local height_percentage, row_start_percentage
|
||||
if type(row_range) == "number" then
|
||||
assert(row_range <= 1)
|
||||
assert(row_range > 0)
|
||||
height_percentage = row_range
|
||||
row_start_percentage = (1 - height_percentage) / 2
|
||||
elseif type(row_range) == "table" then
|
||||
height_percentage = row_range[2] - row_range[1]
|
||||
row_start_percentage = row_range[1]
|
||||
else
|
||||
error(string.format("Invalid type for 'row_range': %p", row_range))
|
||||
end
|
||||
|
||||
default_win_opts.height = math.ceil(vim.o.lines * height_percentage)
|
||||
default_win_opts.row = math.ceil(vim.o.lines * row_start_percentage)
|
||||
|
||||
local width_percentage, col_start_percentage
|
||||
if type(col_range) == "number" then
|
||||
assert(col_range <= 1)
|
||||
assert(col_range > 0)
|
||||
width_percentage = col_range
|
||||
col_start_percentage = (1 - width_percentage) / 2
|
||||
elseif type(col_range) == "table" then
|
||||
width_percentage = col_range[2] - col_range[1]
|
||||
col_start_percentage = col_range[1]
|
||||
else
|
||||
error(string.format("Invalid type for 'col_range': %p", col_range))
|
||||
end
|
||||
|
||||
default_win_opts.col = math.floor(vim.o.columns * col_start_percentage)
|
||||
default_win_opts.width = math.floor(vim.o.columns * width_percentage)
|
||||
|
||||
local bufnr = win_opts.bufnr or vim.api.nvim_create_buf(false, true)
|
||||
local win_id = vim.api.nvim_open_win(bufnr, true, default_win_opts)
|
||||
vim.api.nvim_win_set_buf(win_id, bufnr)
|
||||
|
||||
vim.cmd "setlocal nocursorcolumn"
|
||||
vim.api.nvim_win_set_option(win_id, "winblend", win_opts.winblend)
|
||||
|
||||
local border = Border:new(bufnr, win_id, default_win_opts, border_opts or {})
|
||||
|
||||
_AssociatedBufs[bufnr] = { win_id, border.win_id }
|
||||
|
||||
clear_buf_on_leave(bufnr)
|
||||
|
||||
return {
|
||||
bufnr = bufnr,
|
||||
win_id = win_id,
|
||||
|
||||
border_bufnr = border.bufnr,
|
||||
border_win_id = border.win_id,
|
||||
}
|
||||
end
|
||||
|
||||
function win_float.clear(bufnr)
|
||||
if _AssociatedBufs[bufnr] == nil then
|
||||
return
|
||||
end
|
||||
|
||||
for _, win_id in ipairs(_AssociatedBufs[bufnr]) do
|
||||
if vim.api.nvim_win_is_valid(win_id) then
|
||||
vim.api.nvim_win_close(win_id, true)
|
||||
end
|
||||
end
|
||||
|
||||
_AssociatedBufs[bufnr] = nil
|
||||
end
|
||||
|
||||
return win_float
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
local window = {}
|
||||
|
||||
window.try_close = function(win_id, force)
|
||||
if force == nil then
|
||||
force = true
|
||||
end
|
||||
|
||||
pcall(vim.api.nvim_win_close, win_id, force)
|
||||
end
|
||||
|
||||
window.close_related_win = function(parent_win_id, child_win_id)
|
||||
window.try_close(parent_win_id, true)
|
||||
window.try_close(child_win_id, true)
|
||||
end
|
||||
|
||||
return window
|
||||
Loading…
Add table
Add a link
Reference in a new issue