Repository: nvim-lua/plenary.nvim
Pull request #168 (2021-06-14 18:55:11)

fix: make_relative didn't check for path_sep after cwd
make_relative would falsely produce `bar/file.txt` when
making `/tmp/foo_bar/file.txt` relative to `/tmp/foo` because of the
partial match

Minimal repro:
```
:lua print(require'plenary.path':new("/tmp/foo_bar/b"):make_relative("/tmp/foo/"))
```

Originally found in https://github.com/nvim-telescope/telescope.nvim/pull/913


Patch:
diff --git a/lua/plenary/async/api.lua b/lua/plenary/async/api.lua
new file mode 100644
index 0000000..4a7e84a
--- /dev/null
+++ b/lua/plenary/async/api.lua
@@ -0,0 +1,13 @@
+local a = require('plenary.async.async')
+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
+
+    vim.api[k](...)
+  end
+end})
diff --git a/lua/plenary/async/async.lua b/lua/plenary/async/async.lua
new file mode 100644
index 0000000..490dfad
--- /dev/null
+++ b/lua/plenary/async/async.lua
@@ -0,0 +1,112 @@
+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 = {}
+
+---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(...)
+    assert(type(returned_function) == "function", "type error :: expected func")
+    local stat, msg = pcall(returned_function, vararg.rotate(step, select(3, ...)))
+    if not stat then
+      error(('Failed to call leaf async function: %s'):format(msg))
+    end
+  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(type(async_function) == "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)
+    assert(_PlenaryLeafTable[async_func] == nil, "Async function should not already be in the table")
+    _PlenaryLeafTable[async_func] = true
+  end
+
+  function M.is_leaf_function(async_func)
+    return _PlenaryLeafTable[async_func] ~= nil
+  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 type(func) ~= "function" 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 not (nargs == argc - 1 or nargs == argc) then
+      print(('Expected %s or %s number of arguments, got %s'):format(argc - 1, argc, nargs))
+    end
+
+    if nargs == argc then
+      return func(...)
+    else
+      return co.yield(func, ...)
+    end
+  end
+
+  add_leaf_function(leaf)
+
+  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
+
+---this needs to be fixed
+M.void = function(async_fun)
+  return co.wrap(async_fun)
+end
+
+return M
diff --git a/lua/plenary/async/control.lua b/lua/plenary/async/control.lua
new file mode 100644
index 0000000..094cc22
--- /dev/null
+++ b/lua/plenary/async/control.lua
@@ -0,0 +1,214 @@
+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()
+  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
+  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
+      await(condvar:wait())
+    end
+    counter = counter - 1
+  end
+
+  Receiver.last = 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 = 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:popright()
+    deque:clear()
+    return unpack(val)
+  end
+
+  return Sender, Receiver
+end
+
+return M
diff --git a/lua/plenary/async/init.lua b/lua/plenary/async/init.lua
new file mode 100644
index 0000000..93a08a0
--- /dev/null
+++ b/lua/plenary/async/init.lua
@@ -0,0 +1,42 @@
+---@brief [[
+--- NOTE: This API is still under construction.
+---         It may change in the future :)
+---@brief ]]
+
+local exports = require('plenary.async.async')
+exports.uv = require('plenary.async.uv_async')
+exports.util = require('plenary.async.util')
+exports.lsp = require('plenary.async.lsp')
+exports.api = require('plenary.async.api')
+exports.tests = require('plenary.async.tests')
+exports.control = require('plenary.async.control')
+
+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
diff --git a/lua/plenary/async/lsp.lua b/lua/plenary/async/lsp.lua
new file mode 100644
index 0000000..c0271cd
--- /dev/null
+++ b/lua/plenary/async/lsp.lua
@@ -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
diff --git a/lua/plenary/async/structs.lua b/lua/plenary/async/structs.lua
new file mode 100644
index 0000000..a5c2f97
--- /dev/null
+++ b/lua/plenary/async/structs.lua
@@ -0,0 +1,112 @@
+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
diff --git a/lua/plenary/async/util.lua b/lua/plenary/async/util.lua
new file mode 100644
index 0000000..684e0c1
--- /dev/null
+++ b/lua/plenary/async/util.lua
@@ -0,0 +1,130 @@
+local a = require('plenary.async.async')
+local await = a.await
+local async = a.async
+local vararg = require('plenary.vararg')
+local uv = vim.loop
+-- 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, ret
+
+  a.run(async_function, 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
+
+M.will_block = function(async_func)
+  return function()
+    M.block_on(async_func)
+  end
+end
+
+M.join = function(async_fns)
+  local len = #async_fns
+  local results = {}
+  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 future that when run will select the first async_function that finishes
+---@param async_funs table: The async_function that you want to select
+---@return ...
+M.run_first = a.wrap(function(async_funs, step)
+  local ran = false
+
+  for _, future in ipairs(async_funs) do
+    assert(type(future) == "function", "type error :: future must be function")
+
+    local callback = function(...)
+      if not ran then
+        ran = true
+        step(...)
+      end
+    end
+
+    future(callback)
+  end
+end, 2)
+
+M.run_all = function(async_fns, callback)
+  a.run(function()
+    M.join(async_fns)
+  end, callback)
+end
+
+function M.apcall(async_fn, ...)
+  if a.is_leaf_function(async_fn) then
+    local tx, rx = channel.oneshot()
+    local stat, ret = pcall(async_fn, vararg.rotate(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
diff --git a/lua/plenary/async/uv_async.lua b/lua/plenary/async/uv_async.lua
new file mode 100644
index 0000000..66f3b2c
--- /dev/null
+++ b/lua/plenary/async/uv_async.lua
@@ -0,0 +1,82 @@
+local a = require('plenary.async.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
diff --git a/lua/plenary/fun.lua b/lua/plenary/fun.lua
new file mode 100644
index 0000000..2f46677
--- /dev/null
+++ b/lua/plenary/fun.lua
@@ -0,0 +1,37 @@
+local tbl = require('plenary.tbl')
+
+local M = {}
+
+function M.bind(fn, ...)
+  if select('#', ...) == 1 then
+    local arg = ...
+    return function(...)
+      fn(arg, ...)
+    end
+  end
+
+  local args = tbl.pack(...)
+  return function(...)
+    fn(tbl.unpack(args), ...)
+  end
+end
+
+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
diff --git a/lua/plenary/path.lua b/lua/plenary/path.lua
index a7b0802..cf3d907 100644
--- a/lua/plenary/path.lua
+++ b/lua/plenary/path.lua
@@ -277,17 +277,15 @@ end
 function Path:make_relative(cwd)
   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
-    if #self.filename == #cwd then
-      self.filename = "."
-    else
-      -- skip path separator, unless cwd is root
-      local offset = 2
-      if cwd:sub(-1) == path.sep then
-        offset = 1
-      end
-      self.filename = self.filename:sub(#cwd + offset, -1)
+    if self.filename:sub(1, #cwd) == cwd then
+      self.filename = self.filename:sub(#cwd + 1, -1)
     end
   end
 
diff --git a/lua/plenary/vararg/init.lua b/lua/plenary/vararg/init.lua
new file mode 100644
index 0000000..d60cbd8
--- /dev/null
+++ b/lua/plenary/vararg/init.lua
@@ -0,0 +1,3 @@
+return {
+  rotate = require('plenary.vararg.rotate'),
+}
diff --git a/lua/plenary/vararg/rotate.lua b/lua/plenary/vararg/rotate.lua
new file mode 100644
index 0000000..d840128
--- /dev/null
+++ b/lua/plenary/vararg/rotate.lua
@@ -0,0 +1,75 @@
+---@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[0] = function() return A0 end
+
+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 args = tbl.pack(...)
+    args[#args + 1] = first
+    return tbl.unpack(args)
+end
+
+local function rotate(...)
+    local nargs = select('#', ...)
+
+    return (rotate_lookup[nargs] or rotate_n)(...)
+end
+
+return rotate
diff --git a/scratch/async.lua b/scratch/async.lua
deleted file mode 100644
index 38c7881..0000000
--- a/scratch/async.lua
+++ /dev/null
@@ -1,5 +0,0 @@
-local a = require('plenary.async_lib')
-local async = a.async
-
-async(nil)
--- a.wrap(function() end, nil)
diff --git a/scratch/rotate_bench.lua b/scratch/rotate_bench.lua
new file mode 100644
index 0000000..2b6a9d3
--- /dev/null
+++ b/scratch/rotate_bench.lua
@@ -0,0 +1,21 @@
+local tbl = require('plenary/tbl')
+
+local vararg = require('plenary/vararg')
+local rotate = vararg.rotate
+local bench = require('plenary.profile').benchmark
+
+local function rotate_n(first, ...)
+  local args = tbl.pack(...)
+  args[#args+1] = first
+  return tbl.unpack(args)
+end
+
+local num = 2e7 -- 2e4
+
+print('rotate: ', bench(num, function()
+  local a, b, c, d, e, f, g = rotate(1, 2, 3, 4, 5, 6)
+end))
+
+print('rotate_n: ', bench(num, function()
+  local a, b, c, d, e, f, g = rotate_n(1, 2, 3, 4, 5, 6)
+end))
diff --git a/scripts/update_vararg.py b/scripts/update_vararg.py
new file mode 100755
index 0000000..68f6733
--- /dev/null
+++ b/scripts/update_vararg.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3
+import os
+import subprocess
+
+def generate_file(name, outpath, **kwargs):
+    from jinja2 import Environment, FileSystemLoader
+    env = Environment(loader=FileSystemLoader('./vararg'))
+    template = env.get_template(name)
+    path = os.path.join(outpath, name)
+    with open(path, 'w') as fp:
+        fp.write(template.render(kwargs))
+    subprocess.run(["lua-format", "-i", path])
+
+if __name__ == '__main__':
+    generate_file('rotate.lua', '../lua/plenary/vararg', amount=16)
diff --git a/scripts/vararg/rotate.lua b/scripts/vararg/rotate.lua
new file mode 100644
index 0000000..43066a9
--- /dev/null
+++ b/scripts/vararg/rotate.lua
@@ -0,0 +1,28 @@
+---@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 = {}
+
+{% for n in range(0, amount) %}
+  rotate_lookup[{{n}}] = function ({% for n in range(n) %} A{{n}} {{ ", " if not loop.last else "" }} {% endfor %})
+    return {% for n in range(1, n) %} A{{n}}, {% endfor %} A0
+  end
+{% endfor %}
+
+local function rotate_n(first, ...)
+  local args = tbl.pack(...)
+  args[#args+1] = first
+  return tbl.unpack(args)
+end
+
+local function rotate(...)
+  local nargs = select('#', ...)
+
+  return (rotate_lookup[nargs] or rotate_n)(...)
+end
+
+return rotate


Test patch:
diff --git a/lua/plenary/async/tests.lua b/lua/plenary/async/tests.lua
new file mode 100644
index 0000000..26654c6
--- /dev/null
+++ b/lua/plenary/async/tests.lua
@@ -0,0 +1,14 @@
+local a = require('plenary.async.async')
+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))
+end
+
+return M
diff --git a/lua/plenary/test_harness.lua b/lua/plenary/test_harness.lua
index 35caf63..87aeeb4 100644
--- a/lua/plenary/test_harness.lua
+++ b/lua/plenary/test_harness.lua
@@ -75,7 +75,9 @@ function harness.test_directory(directory, opts)
         string.format('lua require("plenary.busted").run("%s")', p:absolute())
       }
 
-      if opts.minimal_init ~= nil then
+      if opts.minimal ~= nil then
+        table.insert(args, '--noplugin')
+      elseif opts.minimal_init ~= nil then
         table.insert(args, '--noplugin')
 
         table.insert(args, '-u')
diff --git a/tests/plenary/async_lib/channel_spec.lua b/tests/plenary/async/channel_spec.lua
similarity index 57%
rename from tests/plenary/async_lib/channel_spec.lua
rename to tests/plenary/async/channel_spec.lua
index f7da479..ee12a4e 100644
--- a/tests/plenary/async_lib/channel_spec.lua
+++ b/tests/plenary/async/channel_spec.lua
@@ -1,19 +1,21 @@
-require('plenary.async_lib').tests.add_to_env()
-local channel = a.util.channel
+require('plenary.async').tests.add_to_env()
+local channel = a.control.channel
 local eq = assert.are.same
-local protected = a.util.protected
+local apcall = a.util.apcall
 
-a.describe('channel', function()
-  a.describe('oneshot', function()
+describe('channel', function()
+  describe('oneshot', function()
     a.it('should work when rx is used first', function()
       local tx, rx = channel.oneshot()
 
-      a.run(a.future(function()
-        local got = await(rx())
+      a.run(function()
+        local got = rx()
+
         eq("sent value", got)
-      end))
+      end)
 
       tx("sent value")
+
     end)
 
     a.it('should work when tx is used first', function()
@@ -21,39 +23,52 @@ a.describe('channel', function()
 
       tx("sent value")
 
-      a.run(a.future(function()
-        local got = await(rx())
-        eq("sent value", got)
-      end))
+      local got = rx()
+
+      eq("sent value", got)
     end)
 
     a.it('should work with multiple returns', function()
       local tx, rx = channel.oneshot()
 
-      a.run(a.future(function()
-        local got, got2 = await(rx())
+      a.run(function()
+        local got, got2 = rx()
         eq("sent value", got)
         eq("another sent value", got2)
-      end))
+      end)
 
       tx("sent value", "another sent value")
     end)
 
+    a.it('should work when sending a falsey value', function ()
+      local tx, rx = channel.oneshot()
+
+      tx(false)
+
+      local res = rx()
+      eq(res, false)
+
+      local stat, ret = apcall(rx)
+      eq(stat, false)
+      local stat, ret = apcall(rx)
+      eq(stat, false)
+    end)
+
     a.it('should work when sending a nil value', function ()
       local tx, rx = channel.oneshot()
 
       tx(nil)
 
-      local res = await(rx())
+      local res = rx()
       eq(res, nil)
 
-      local stat, ret = await(protected(rx()))
+      local stat, ret = apcall(rx)
       eq(stat, false)
-      local stat, ret = await(protected(rx()))
+      local stat, ret = apcall(rx)
       eq(stat, false)
     end)
 
-    a.it('should block sending mulitple times', function()
+    a.it('should error when sending mulitple times', function()
       local tx, rx = channel.oneshot()
 
       tx()
@@ -61,16 +76,16 @@ a.describe('channel', function()
       eq(stat, false)
     end)
 
-    a.it('should block receiving multiple times', function ()
+    a.it('should block receiving multiple times', function()
       local tx, rx = channel.oneshot()
-      tx()
-      await(rx())
-      local stat = await(protected(rx()))
+      tx(true)
+      rx()
+      local stat = apcall(rx)
       eq(stat, false)
     end)
   end)
 
-  a.describe('counter', function()
+  describe('counter', function()
     a.it('should work', function()
       local tx, rx = channel.counter()
 
@@ -80,15 +95,13 @@ a.describe('channel', function()
 
       local counter = 0
 
-      local recv_stuff = async(function()
+      a.run(function()
         for i = 1, 3 do
-          await(rx.recv())
+          rx.recv()
           counter = counter + 1
         end
       end)
 
-      a.run(recv_stuff())
-
       eq(counter, 3)
     end)
 
@@ -101,15 +114,11 @@ a.describe('channel', function()
 
       local counter = 0
 
-      local recv_stuff = async(function()
-        for i = 1, 3 do
-          await(rx.last())
-          counter = counter + 1
-        end
+      a.run(function()
+        rx.last()
+        counter = counter + 1
       end)
 
-      a.run(recv_stuff())
-
       eq(counter, 1)
     end)
   end)
diff --git a/tests/plenary/async_lib/condvar_spec.lua b/tests/plenary/async/condvar_spec.lua
similarity index 56%
rename from tests/plenary/async_lib/condvar_spec.lua
rename to tests/plenary/async/condvar_spec.lua
index 1793c29..afe2f55 100644
--- a/tests/plenary/async_lib/condvar_spec.lua
+++ b/tests/plenary/async/condvar_spec.lua
@@ -1,20 +1,19 @@
-require('plenary.async_lib').tests.add_to_env()
-local Condvar = a.util.Condvar
+require('plenary.async').tests.add_to_env()
+local Condvar = a.control.Condvar
 local eq = assert.are.same
+local join, run_all = a.util.join, a.util.run_all
 
-a.describe('condvar', function()
+describe('condvar', function()
   a.it('should allow blocking', function()
     local var = false
 
     local condvar = Condvar.new()
 
-    local blocking = async(function()
-      await(condvar:wait())
+    a.run(function()
+      condvar:wait()
       var = true
     end)
 
-    a.run(blocking())
-
     eq(var, false)
 
     condvar:notify_one()
@@ -27,22 +26,24 @@ a.describe('condvar', function()
 
     local condvar = Condvar.new()
 
-    local first = async(function()
-      await(condvar:wait())
+    local first = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    local second = async(function()
-      await(condvar:wait())
+    local second = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    local third = async(function()
-      await(condvar:wait())
+    local third = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    a.run_all { first(), second(), third() }
+    a.run(function()
+      join { first, second, third }
+    end)
 
     eq(0, counter)
 
@@ -64,22 +65,22 @@ a.describe('condvar', function()
 
     local condvar = Condvar.new()
 
-    local first = async(function()
-      await(condvar:wait())
+    local first = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    local second = async(function()
-      await(condvar:wait())
+    local second = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    local third = async(function()
-      await(condvar:wait())
+    local third = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    a.run_all { first(), second(), third() }
+    run_all { first, second, third }
 
     eq(0, counter)
 
@@ -101,22 +102,22 @@ a.describe('condvar', function()
 
     local condvar = Condvar.new()
 
-    local first = async(function()
-      await(condvar:wait())
+    local first = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    local second = async(function()
-      await(condvar:wait())
+    local second = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    local third = async(function()
-      await(condvar:wait())
+    local third = function()
+      condvar:wait()
       counter = counter + 1
-    end)
+    end
 
-    a.run_all { first(), second(), third() }
+    run_all { first, second, third }
 
     eq(0, counter)
 
diff --git a/tests/plenary/async_lib/deque_spec.lua b/tests/plenary/async/deque_spec.lua
similarity index 95%
rename from tests/plenary/async_lib/deque_spec.lua
rename to tests/plenary/async/deque_spec.lua
index ae14f83..3143037 100644
--- a/tests/plenary/async_lib/deque_spec.lua
+++ b/tests/plenary/async/deque_spec.lua
@@ -1,4 +1,4 @@
-local Deque = require('plenary.async_lib.structs').Deque
+local Deque = require('plenary.async.structs').Deque
 local eq = assert.are.same
 
 -- just a helper to create the test deque
diff --git a/tests/plenary/async_lib/read_file_bench.lua b/tests/plenary/async/read_file_bench.lua
similarity index 98%
rename from tests/plenary/async_lib/read_file_bench.lua
rename to tests/plenary/async/read_file_bench.lua
index c4b0d37..a976c73 100644
--- a/tests/plenary/async_lib/read_file_bench.lua
+++ b/tests/plenary/async/read_file_bench.lua
@@ -1,5 +1,5 @@
 -- do not run this for now, the asset was removed
-local a = require('plenary.async_lib')
+local a = require('plenary.async')
 local async = a.async
 local await = a.await
 local await_all = a.await_all
diff --git a/tests/plenary/async_lib/semaphore_spec.lua b/tests/plenary/async/semaphore_spec.lua
similarity index 54%
rename from tests/plenary/async_lib/semaphore_spec.lua
rename to tests/plenary/async/semaphore_spec.lua
index f3df6a4..3c23031 100644
--- a/tests/plenary/async_lib/semaphore_spec.lua
+++ b/tests/plenary/async/semaphore_spec.lua
@@ -1,9 +1,9 @@
-require('plenary.async_lib').tests.add_to_env()
-local Semaphore = a.util.Semaphore
+require('plenary.async').tests.add_to_env()
+local Semaphore = a.control.Semaphore
 
 local eq = assert.are.same
 
-a.describe('semaphore', function()
+describe('semaphore', function()
   a.it('should validate arguments', function()
     local status = pcall(Semaphore.new, -1)
     eq(status, false)
@@ -13,44 +13,44 @@ a.describe('semaphore', function()
   end)
 
   a.it('should acquire a permit if available', function ()
-    local sem = a.util.Semaphore.new(1)
-    local permit = await(sem:acquire())
+    local sem = Semaphore.new(1)
+    local permit = sem:acquire()
     assert(permit ~= nil)
   end)
 
   a.it('should block if no permit is available', function ()
-    local sem = a.util.Semaphore.new(1)
-    await(sem:acquire())
-    
+    local sem = Semaphore.new(1)
+    sem:acquire()
+
     local completed = false
-    local blocking = async(function ()
-      await(sem:acquire())
+    local blocking = function ()
+      sem:acquire()
       completed = true
-    end)
-    a.run(blocking())
+    end
+    a.run(blocking)
 
     eq(completed, false)
   end)
 
   a.it('should give another permit when an acquired permit is released', function ()
-    local sem = a.util.Semaphore.new(1)
-    local permit = await(sem:acquire())
+    local sem = Semaphore.new(1)
+    local permit = sem:acquire()
     permit:forget()
-    local next_permit = await(sem:acquire())
+    local next_permit = sem:acquire()
     assert(next_permit ~= nil)
   end)
 
   a.it('should permit the next waiting client when a permit is released', function () 
-    local sem = a.util.Semaphore.new(1)
-    local permit = await(sem:acquire())
+    local sem = Semaphore.new(1)
+    local permit = sem:acquire()
 
     local completed = false
-    local blocking = async(function ()
-      await(sem:acquire())
+    local blocking = function ()
+      sem:acquire()
       completed = true
-    end)
+    end
 
-    a.run(blocking())
+    a.run(blocking)
     permit:forget()
 
     eq(completed, true)
diff --git a/tests/plenary/async/util_spec.lua b/tests/plenary/async/util_spec.lua
new file mode 100644
index 0000000..b0b0da0
--- /dev/null
+++ b/tests/plenary/async/util_spec.lua
@@ -0,0 +1,50 @@
+require('plenary.async').tests.add_to_env()
+local block_on = a.util.block_on
+local eq = assert.are.same
+local id = a.util.id
+
+describe('async await util', function()
+  describe('block_on', function()
+    a.it('should block_on', function()
+      local fn = function()
+        a.util.sleep(100)
+        return 'hello'
+      end
+
+      local res = fn()
+      eq(res, 'hello')
+    end)
+
+    a.it('should work even when failing', function ()
+      local nonleaf = function()
+        eq(true, false)
+      end
+
+      local stat = pcall(block_on, nonleaf)
+      eq(stat, false)
+    end)
+  end)
+
+  describe('protect', function()
+    a.it('should be able to protect a non-leaf future', function()
+      local nonleaf = function()
+        error("This should error")
+        return 'return'
+      end
+
+      local stat, ret = pcall(nonleaf)
+      eq(false, stat)
+      assert(ret:match("This should error"))
+    end)
+
+    a.it('should be able to protect a non-leaf future that doesnt fail', function()
+      local nonleaf = function()
+        return 'didnt fail'
+      end
+
+      local stat, ret = pcall(nonleaf)
+      eq(stat, true)
+      eq(ret, 'didnt fail')
+    end)
+  end)
+end)
diff --git a/tests/plenary/async_lib/util_spec.lua b/tests/plenary/async_lib/util_spec.lua
deleted file mode 100644
index ccbc1b6..0000000
--- a/tests/plenary/async_lib/util_spec.lua
+++ /dev/null
@@ -1,104 +0,0 @@
-require('plenary.async_lib').tests.add_to_env()
-local block_on = a.util.block_on
-local eq = assert.are.same
-local id = a.util.id
-
-a.describe('async await util', function()
-  a.describe('block_on', function()
-    a.it('should block_on', function()
-      local fn = async(function()
-        await(a.util.sleep(100))
-        return 'hello'
-      end)
-
-      local res = block_on(fn())
-      eq(res, 'hello')
-    end)
-
-    a.it('should work even when failing', function ()
-      local nonleaf = async(function()
-        eq(true, false)
-      end)
-
-      local stat = pcall(block_on, nonleaf())
-      eq(stat, false)
-    end)
-  end)
-
-  a.describe('protect', function()
-    a.it('should be able to protect a non-leaf future', function()
-      local nonleaf = async(function()
-        error("This should error")
-        return 'return'
-      end)
-
-      local stat, ret = await(a.util.protected_non_leaf(nonleaf()))
-      eq(false, stat)
-      assert(ret:match("This should error"))
-    end)
-
-    a.it('should be able to protect a non-leaf future that doesnt fail', function()
-      local nonleaf = async(function()
-        return 'didnt fail'
-      end)
-
-      local stat, ret = await(a.util.protected_non_leaf(nonleaf()))
-      eq(stat, true)
-      eq(ret, 'didnt fail')
-    end)
-
-    a.it('should be able to protect a leaf future', function()
-      local leaf = a.wrap(function(callback)
-        error("This should error")
-        callback()
-      end, 1)
-
-      local stat, ret = await(a.util.protected(leaf()))
-      eq(stat, false)
-      assert(ret:match("This should error") ~= nil)
-    end)
-
-    a.it('should be able to protect a leaf future that doesnt fail', function()
-      local fn = a.wrap(function(callback)
-        callback('didnt fail')
-      end, 1)
-
-      local stat, ret = await(a.util.protected(fn()))
-      eq(stat, true)
-      eq(ret, 'didnt fail')
-    end)
-  end)
-
-  a.describe('timeout', function()
-    a.it('should block one and work', function()
-      local timed_out = await(a.util.timeout(a.util.sleep(1000), 500))
-
-      print('timed out 2:', timed_out)
-
-      assert(timed_out == true)
-    end)
-
-    a.it('should work when timeout is longer', function ()
-      local timed_out = await(a.util.timeout(a.util.sleep(100), 1000))
-
-      print('timed out 1:', timed_out)
-
-      assert(timed_out == false)
-    end)
-  end)
-
-  a.it('id should work', function()
-    eq(await(id(1, 2, 3, 4, 5)), 1, 2, 3, 4, 5)
-  end)
-
-  a.it('yield_now should work', function ()
-    local yield_now = a.util.yield_now
-    yield_now()
-    yield_now()
-    yield_now()
-    yield_now()
-    yield_now()
-    yield_now()
-    yield_now()
-  end)
-end)
diff --git a/tests/plenary/path_spec.lua b/tests/plenary/path_spec.lua
index 571d89f..063ae90 100644
--- a/tests/plenary/path_spec.lua
+++ b/tests/plenary/path_spec.lua
@@ -154,6 +154,20 @@ describe('Path', function()
       local relative = Path:new(p.filename):make_relative(p.filename)
       assert.are.same(relative, ".")
     end)
+
+    it('should not truncate if path separator is not present after cwd', function()
+      local cwd = "tmp" .. path.sep .. "foo"
+      local p = Path:new { 'tmp', 'foo_bar', 'fileb.lua'}
+      local relative = Path:new(p.filename):make_relative(cwd)
+      assert.are.same(p.filename, relative)
+    end)
+
+    it('should not truncate if path separator is not present after cwd and cwd ends in path sep', function()
+      local cwd = "tmp" .. path.sep .. "foo" .. path.sep
+      local p = Path:new { 'tmp', 'foo_bar', 'fileb.lua'}
+      local relative = Path:new(p.filename):make_relative(cwd)
+      assert.are.same(p.filename, relative)
+    end)
   end)
 
   describe(':normalize', function()
diff --git a/tests/plenary/rotate_spec.lua b/tests/plenary/rotate_spec.lua
new file mode 100644
index 0000000..44c2f9e
--- /dev/null
+++ b/tests/plenary/rotate_spec.lua
@@ -0,0 +1,18 @@
+local rotate = require('plenary.vararg').rotate
+
+local eq = vim.deep_equal
+
+describe('rotate', function ()
+  it('should rotate varargs', function ()
+    eq({rotate(1, 2, 3)}, {2, 3, 1})
+    eq({rotate(1, 2, 3, 4, 5, 6, 7, 8, 9)}, {2, 3, 4, 5, 6, 7, 8, 9, 1})
+  end)
+
+  it('should rotate none', function ()
+    eq({rotate()}, {})
+  end)
+
+  it('should rotate one', function ()
+    eq({rotate(1)}, {1})
+  end)
+end)