Repository: nvim-lua/plenary.nvim
Pull request #135 (2021-04-13 02:39:04)

Plenary enum
This PR introduces the `plenary.enum` module, which can be used to easily and idiomatically create enum-like tables, like so:

```lua
local Enum = require 'plenary.enum'

local MyEnum = Enum {
 'Foo',
 'Bar',
 'Qux'
}

print(Enum.is_enum(MyEnum)) -- true

-- You can also access the members by name or by value
print(MyEnum.Foo, MyEnum[1], MyEnum[MyEnum.Foo]) -- 1        Foo          Foo

-- You can also define a specific value for members
local MyEnum2 = Enum {
 {'Foo', 10},
 'Bar',
 'Qux',
}
print(MyEnum2.Foo, MyEnum2.Bar, MyEnum2.Qux)  -- 10         11            12
```

oberblastmeister: Maybe make each variant an immutable metatable with `__eq` that can only compare with each other? Then you can add a function that will get the id of a variant
oberblastmeister: It would also be nice if you could set the starting number like in `typescript` or `rust`. Maybe it can look like this:
```lua
-- this will make everything start from 6
Enum {
  {'first', 6}
  'second',
  'third',
  'fourth',
}
```
Joakker: That should do it. I've updated the original comment to document this
Joakker: That should do it. Let me know if I should change something 😃 

oberblastmeister: Thanks for the help on creating enums. I think your implementation is too complicated and also has a lot of edge cases though. Given this enum
```lua
    local E = Enum{
      {'Foo', 3},
      {'Bar', 2},
      'Qux',
      'Baz',
      {'Another', 11}
    }
```
`Qux` will have the same id as `Foo` which is a big issue. Also, there are no checks if there are duplicated name for a variant. I also don't know why two tables are created, one for values and one for the enum. I don't like that each variant has the `__add` metamethod. I made a new branch here https://github.com/oberblastmeister/plenary.nvim/tree/plenary-enum that addresses the concerns. You can polish it if you want to or we can use it.
Joakker: I've looked over it, and I think your implementation is more concise and a bit better. I've merged it and I'll be adding a couple utility methods
oberblastmeister: I think we should add an `Enum:is` method that is the same as `==` but does more checks because `==` will just silently fail
Joakker: You mean something like this?
```lua
local Enum = require 'plenary.enum'

local myEnum = Enum{
 'Foo',
 'Bar',
 'Qux'
}

print(myEnum:is('Foo', 1)) -- true
print(myEnum:is('Bar', 3)) -- false
```
oberblastmeister: We need better error messages and more docs. Also you should add some tests to check that it should error correctly. Other than that, it looks good to me
Joakker: Ok, I think that should do it
oberblastmeister: I feel like we should add some functions like `from_str` and `from_num` do be able to get a variant and if the key is invalid it would just return nil instead of erroring out. We can probably use `has_key` for that
Joakker: The latest CI seems to fail the test, but the enum ones specifically don't error out
tjdevries: What does this do:

```lua
local E = Enum { { 'Foo', 5 } }

print(E.Foo == 5 )
```
Joakker: The `E` enum is created with the `Foo` member set to represent 5, however since `E.Foo` is an object and not a number, it can only be compared to other members of `E`. If you want to check if it does stand for 5, you can compare against the `:id()` method, which returns the value given initially. Like so

```lua
local E = Enum { {'Foo', 5} }
print(E.Foo == 5)      -- Table and number, false
print(E.Foo:id() == 5) -- Number and number, true
```
tjdevries: I don't love `:id()`, can we change it to `.value`? Then I'm thinking we're pretty close to the v1 of what enums should do. I may make some changes afterwards to add some new functionality, but I like what you've done here.
Joakker: There we go

Patch:
diff --git a/README.md b/README.md
index 1b671a3..1e4ee71 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ Plug 'nvim-lua/plenary.nvim'
 - `plenary.context_manager`
 - `plenary.test_harness`
 - `plenary.filetype`
+- `plenary.strings`
 
 ### plenary.job
 
@@ -179,6 +180,19 @@ return {
 }
 ```
 
+### plenary.strings
+
+Re-implement VimL funcs to use them in Lua loop.
+
+* `strings.strdisplaywidth`
+* `strings.strcharpart`
+
+And some other funcs are here to deal with common problems.
+
+* `strings.truncate`
+* `strings.align_str`
+* `strings.dedent`
+
 ### plenary.popup
 
 `popup_*` clone of Vim's commands. If it gets good enough, will submit PR to Neovim and write C wrappers
@@ -192,6 +206,23 @@ Window helper functions to wrap some of the more difficult cases. Particularly f
 
 Status: WIP
 
+### plenary.collections
+
+Contains pure lua implementations for various standard collections.
+
+```lua
+local List = require 'plenary.collections.py_list'
+
+local myList = List { 9, 14, 32, 5 }
+
+for i, v in myList:iter() do
+    print(i, v)
+end
+
+```
+
+Status: WIP
+
 ### Troubleshooting
 
 If you're having trouble / things are hanging / other problems:
@@ -205,4 +236,3 @@ This will enable debuggin for the plugin.
 ### plenary.neorocks
 
 DELETED: Please use packer.nvim or other lua-rocks wrapper instead. This no longer exists.
-
diff --git a/data/plenary/filetypes/builtin.lua b/data/plenary/filetypes/builtin.lua
index 600a6f5..9947aba 100644
--- a/data/plenary/filetypes/builtin.lua
+++ b/data/plenary/filetypes/builtin.lua
@@ -3,6 +3,7 @@ local shebang_fts = {
   ['sh'] = 'sh',
   ['bash'] = 'sh',
   ['zsh'] = 'zsh',
+  ['fish'] = 'fish',
   ['python'] = 'python',
   ['python2'] = 'python',
   ['python3'] = 'python',
@@ -32,6 +33,7 @@ return {
     ['coffee'] = 'coffee',
     ['_coffee'] = 'coffee',
     ['nix'] = 'nix',
+    ['erb'] = 'eruby',
   },
   file_name = {
     ['cakefile'] = 'coffee',
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/collections/py_list.lua b/lua/plenary/collections/py_list.lua
new file mode 100644
index 0000000..06d9bec
--- /dev/null
+++ b/lua/plenary/collections/py_list.lua
@@ -0,0 +1,363 @@
+---@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,
+})
diff --git a/lua/plenary/curl.lua b/lua/plenary/curl.lua
index ed8bbec..563c82c 100644
--- a/lua/plenary/curl.lua
+++ b/lua/plenary/curl.lua
@@ -154,7 +154,7 @@ parse.request = function(opts)
     elseif P.is_file(P.new(b)) then
       opts.in_file = b
     elseif type(b) == "string" then
-      opts.raw = b
+      opts.raw_body = b
     end
   end
   return vim.tbl_flatten({
@@ -163,7 +163,7 @@ parse.request = function(opts)
     parse.method(opts.method),
     parse.headers(opts.headers),
     parse.accept_header(opts.accept),
-    parse.raw_body(opts.raw),
+    parse.raw_body(opts.raw_body),
     parse.data_body(opts.data),
     parse.form(opts.form),
     parse.file(opts.in_file),
diff --git a/lua/plenary/enum.lua b/lua/plenary/enum.lua
new file mode 100644
index 0000000..ad7aa3c
--- /dev/null
+++ b/lua/plenary/enum.lua
@@ -0,0 +1,145 @@
+---@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,
+})
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/job.lua b/lua/plenary/job.lua
index 267dd35..757c1c2 100644
--- a/lua/plenary/job.lua
+++ b/lua/plenary/job.lua
@@ -3,6 +3,20 @@ local uv = vim.loop
 
 local F = require('plenary.functional')
 
+---@class Job
+---@field command string          : Command to run
+---@field args Array              : List of arguments to pass
+---@field cwd string              : Working directory for job
+---@field env Map|Array           : Environment looking like: { ['VAR'] = 'VALUE } or { 'VAR=VALUE' }
+---@field skip_validation boolean : Skip validating the arguments
+---@field enable_handlers boolean : If set to false, disables all callbacks associated with output
+---@field on_start function       : Run when starting job
+---@field on_stdout function      : (error: string, data: string, self? Job)
+---@field on_stderr function      : (error: string, data: string, self? Job)
+---@field on_exit function        : (self, 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.
+---@field enabled_recording boolean
 local Job = {}
 Job.__index = Job
 
@@ -61,22 +75,9 @@ end
 ---@class Map
 --- Map-like table
 
---- Create a new job
----
----@class Job
----@class o
----@field command string          : Command to run
----@field args Array              : List of arguments to pass
----@field cwd string              : Working directory for job
----@field env Map|Array           : Environment looking like: { ['VAR'] = 'VALUE } or { 'VAR=VALUE' }
----@field skip_validation boolean : Skip validating the arguments
----@field enable_handlers boolean : If set to false, disables all callbacks associated with output
----@field on_start function       : Run when starting job
----@field on_stdout function      : (error: string, data: string, self? Job)
----@field on_stderr function      : (error: string, data: string, self? Job)
----@field on_exit function        : (self, 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.
+---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"))
diff --git a/lua/plenary/path.lua b/lua/plenary/path.lua
index 2de31c2..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
 
@@ -341,7 +339,7 @@ local shorten = (function()
     end
   end
   return function(filename)
-    shorten_len(filename, 1)
+    return shorten_len(filename, 1)
   end
 end)()
 
@@ -613,7 +611,10 @@ function Path:head(lines)
   local fd = uv.fs_open(self:expand(), "r", 438)
   if not fd then return end
   local stat = assert(uv.fs_fstat(fd))
-  if stat.type ~= 'file' then return nil end
+  if stat.type ~= 'file' then
+    uv.fs_close(fd)
+    return nil
+  end
 
   local data = ''
   local index, count = 0, 0
@@ -647,7 +648,10 @@ function Path:tail(lines)
   local fd = uv.fs_open(self:expand(), "r", 438)
   if not fd then return end
   local stat = assert(uv.fs_fstat(fd))
-  if stat.type ~= 'file' then return nil end
+  if stat.type ~= 'file' then
+    uv.fs_close(fd)
+    return nil
+  end
 
   local data = ''
   local index, count = stat.size - 1, 0
diff --git a/lua/plenary/scandir.lua b/lua/plenary/scandir.lua
index 554f8c7..91d6eec 100644
--- a/lua/plenary/scandir.lua
+++ b/lua/plenary/scandir.lua
@@ -80,9 +80,11 @@ local process_item = function(opts, name, typ, current_dir, next_dir, bp, data,
         table.insert(next_dir, entry)
       end
       if opts.add_dirs then
-        if not msp or msp(entry) then
-          table.insert(data, entry)
-          if opts.on_insert then opts.on_insert(entry, typ) end
+        if not giti or interpret_gitignore(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
     else
@@ -134,11 +136,12 @@ m.scan_dir = function(path, opts)
   repeat
     local current_dir = table.remove(next_dir, 1)
     local fd = uv.fs_scandir(current_dir)
-    if fd == nil then break end
-    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_seach_pat)
+    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_seach_pat)
+      end
     end
   until table.getn(next_dir) == 0
   return data
@@ -292,7 +295,13 @@ local get_username = (function()
 
     return function(tbl, id)
       if tbl[id] then return tbl[id] end
-      local name = ffi.string(ffi.C.getpwuid(id).pw_name)
+      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
@@ -324,7 +333,13 @@ local get_groupname = (function()
 
     return function(tbl, id)
       if tbl[id] then return tbl[id] end
-      local name = ffi.string(ffi.C.getgrgid(id).gr_name)
+      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
diff --git a/lua/plenary/strings.lua b/lua/plenary/strings.lua
new file mode 100644
index 0000000..1fd3e8e
--- /dev/null
+++ b/lua/plenary/strings.lua
@@ -0,0 +1,167 @@
+local path = require('plenary.path').path
+
+local M = {}
+
+M.strdisplaywidth = (function()
+  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);
+    ]]
+
+    return 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
+  else
+    return 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
+  end
+end)()
+
+M.strcharpart = (function()
+  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
+
+    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 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
+  end
+end)()
+
+M.truncate = function(str, len, dots)
+  str = tostring(str) -- We need to make sure its an actually a string and not a number
+  dots = dots or '…'
+  if M.strdisplaywidth(str) <= len then
+    return str
+  end
+  local start = 0
+  local current = 0
+  local result = ''
+  local len_of_dots = M.strdisplaywidth(dots)
+  while true do
+    local part = M.strcharpart(str, start, 1)
+    current = current + M.strdisplaywidth(part)
+    if (current + len_of_dots) > len then
+      result = result .. dots
+      break
+    end
+    result = result .. part
+    start = start + 1
+  end
+  return result
+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
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/lua/plenary/window/border.lua b/lua/plenary/window/border.lua
index 13c7e84..c447a12 100644
--- a/lua/plenary/window/border.lua
+++ b/lua/plenary/window/border.lua
@@ -1,4 +1,5 @@
 local tbl = require('plenary.tbl')
+local strings = require('plenary.strings')
 
 local Border = {}
 
@@ -29,14 +30,23 @@ function Border._create_lines(content_win_options, border_win_options)
 
   if content_win_options.row > 0 then
     if border_win_options.title then
+      local title_len
       local title = border_win_options.title
-      if title ~= '' then
+      if title == '' then
+        title_len = 0
+      else
+        local len = strings.strdisplaywidth(title)
+        local max_title_width = content_win_options.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 title_len = string.len(title)
 
       local midpoint = math.floor(content_win_options.width / 2)
-      local left_start = midpoint - math.floor(title_len / 2)
+      local left_start = midpoint - math.ceil(title_len / 2)
 
       topline = string.format("%s%s%s%s%s",
         topleft,
@@ -136,13 +146,13 @@ function Border:new(content_bufnr, content_win_id, content_win_options, border_w
   })
 
   vim.cmd(string.format(
-    "autocmd BufLeave,BufDelete <buffer=%s> ++nested ++once :lua require('plenary.window').close_related_win(%s, %s)",
+    "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,WinLeave <buffer=%s> ++nested ++once :lua require('plenary.window').try_close(%s, true)",
+    "autocmd WinClosed <buffer=%s> ++nested ++once :lua require('plenary.window').try_close(%s, true)",
     content_bufnr,
     obj.win_id))
 
diff --git a/lua/plenary/window/float.lua b/lua/plenary/window/float.lua
index 31e62c5..97f3404 100644
--- a/lua/plenary/window/float.lua
+++ b/lua/plenary/window/float.lua
@@ -137,11 +137,13 @@ 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
-function win_float.percentage_range_window(col_range, row_range, options)
-  options = tbl.apply_defaults(options, win_float.default_options)
+--@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 win_opts = win_float.default_opts(options)
-  win_opts.relative = "editor"
+  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
@@ -156,8 +158,8 @@ function win_float.percentage_range_window(col_range, row_range, options)
     error(string.format("Invalid type for 'row_range': %p", row_range))
   end
 
-  win_opts.height = math.ceil(vim.o.lines * height_percentage)
-  win_opts.row = math.ceil(vim.o.lines *  row_start_percentage)
+  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
@@ -172,17 +174,17 @@ function win_float.percentage_range_window(col_range, row_range, options)
     error(string.format("Invalid type for 'col_range': %p", col_range))
   end
 
-  win_opts.col = math.floor(vim.o.columns * col_start_percentage)
-  win_opts.width = math.floor(vim.o.columns * width_percentage)
+  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 = options.bufnr or vim.api.nvim_create_buf(false, true)
-  local win_id = vim.api.nvim_open_win(bufnr, true, win_opts)
+  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', options.winblend)
+  vim.api.nvim_win_set_option(win_id, 'winblend', win_opts.winblend)
 
-  local border = Border:new(bufnr, win_id, win_opts, {})
+  local border = Border:new(bufnr, win_id, default_win_opts, border_opts or {})
 
   _AssociatedBufs[bufnr] = { win_id, border.win_id, }
 
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/curl_spec.lua b/tests/plenary/curl_spec.lua
index c19923b..786a723 100644
--- a/tests/plenary/curl_spec.lua
+++ b/tests/plenary/curl_spec.lua
@@ -151,6 +151,22 @@ describe('CURL Wrapper:', function()
       eq(json, vim.fn.json_decode(res).json)
     end)
 
+
+    it("should not include the body twice", function ()
+      local json = { title = "New", name = "YORK" }
+      local body = vim.fn.json_encode(json)
+      local res = curl.post("https://postman-echo.com/post", {
+        body = body,
+        headers = {
+          content_type = "application/json"
+        },
+        dry_run = true,
+      })
+      local joined_response = table.concat(res, " ")
+      local first_index = joined_response:find(body)
+
+      eq(nil, joined_response:find(body, first_index + 1))
+    end)
   end)
   describe("PUT", function() --------------------------------------------------
 
diff --git a/tests/plenary/enum_spec.lua b/tests/plenary/enum_spec.lua
new file mode 100644
index 0000000..1de4faf
--- /dev/null
+++ b/tests/plenary/enum_spec.lua
@@ -0,0 +1,93 @@
+local Enum = require 'plenary.enum'
+
+local function should_fail(fun)
+  local stat = pcall(fun)
+  assert(not stat, "Function should fail")
+end
+
+describe('Enum', function()
+  it('should be able to define specific values for members', function()
+    local E = Enum{
+      {'Foo', 2},
+      {'Bar', 4},
+      'Qux',
+      'Baz',
+      {'Another', 11}
+    }
+
+    assert(E.Foo.value == 2)
+    assert(E.Bar.value == 4)
+    assert(E.Qux.value == 5)
+    assert(E.Baz.value == 6)
+    assert(E.Another.value == 11)
+
+    assert(E[2] == 'Foo')
+    assert(E[4] == 'Bar')
+    assert(E[5] == 'Qux')
+    assert(E[6] == 'Baz')
+    assert(E[11] == 'Another')
+  end)
+  it('should compare with itself', function()
+    local E1 = Enum {
+      'Foo',
+      {'Qux', 11},
+      'Bar',
+      'Baz',
+    }
+
+    local E2 = Enum {
+      'Foo',
+      'Bar',
+      'Baz',
+    }
+
+    assert(E1.Foo < E1.Qux)
+    assert(E1.Baz > E1.Bar)
+
+    assert(not (E1.Foo == E2.Foo))
+
+    should_fail(function()
+      return E1.Foo > E2.Foo
+    end)
+
+    should_fail(function()
+      return E2.Bar >= E1.Foo
+    end)
+  end)
+  it('should error when accessing invalid field', function()
+    local E = Enum {
+      'Foo',
+      'Bar',
+      'Baz',
+    }
+
+    should_fail(function()
+      return E.foo
+    end)
+
+    should_fail(function()
+      return E.bar
+    end)
+  end)
+  it('should fail if there is name or index clashing', function()
+    should_fail(function()
+      return Enum{
+        'Foo',
+        'Foo'
+      }
+    end)
+    should_fail(function()
+      return Enum{
+        'Foo',
+        {'Bar', 1}
+      }
+    end)
+  end)
+  it('should fail if there is a key that starts with lowercase', function()
+    should_fail(function()
+      return Enum{
+        'foo'
+      }
+    end)
+  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/py_list_spec.lua b/tests/plenary/py_list_spec.lua
new file mode 100644
index 0000000..c90485c
--- /dev/null
+++ b/tests/plenary/py_list_spec.lua
@@ -0,0 +1,61 @@
+local List = require('plenary.collections.py_list')
+local Iter = require('plenary.iterators')
+
+describe('List', function()
+  it('should detect whether a value is an instance of List', function()
+    local l = List {1, 2}
+    local n = 42
+    assert(List.is_list(l))
+    assert(not List.is_list(n))
+  end)
+  it('should be equal if all elements are equal', function()
+    local l1 = List {1, 2, 3}
+    local l2 = List {1, 2, 3}
+    local l3 = List {4, 5, 6}
+    assert.are.equal(l1, l2)
+    assert.are_not.equal(l1, l3)
+  end)
+  it('can be concatenated to other list-like tables', function()
+    local l1 = List {1, 2, 3} .. {4}
+    assert.are.equal(l1, List {1, 2, 3, 4})
+  end)
+  it('can create a copy of itself with equal elements', function()
+    local l1 = List {1, 2, 3}
+    local l2 = l1:copy()
+    assert.are.equal(l1, l2)
+  end)
+  it('can create a slice between two indices', function()
+    local l1 = List {1, 2, 3, 4}
+    local l2 = l1:slice(2, 4)
+    assert.are.equal(l2, List {2, 3, 4})
+  end)
+  it('can reverse itself in place', function()
+    local l = List {1, 2, 3, 4}
+    l:reverse()
+    assert.are.equal(l, List {4, 3, 2, 1})
+  end)
+  it('can push elements to itself', function()
+    local l = List {1, 2, 3}
+    l:push(4)
+    assert.are.equal(l, List {1, 2, 3, 4})
+  end)
+  it('can pop the n-th element from itself (last one by default)', function()
+    local l = List {1, 2, 3, 4}
+    local n = l:pop()
+    assert.are.equal(l, List {1, 2, 3})
+    assert.are.equal(n, 4)
+  end)
+  it('can create a list from an iterable', function()
+    local l = List.from_iter(Iter.range(5, 10))
+    assert.are.equal(l, List {5, 6, 7, 8, 9, 10})
+  end)
+  it('can be partitioned based on a predicate', function()
+    local l = List.from_iter(Iter.range(1, 10))
+    local evens, odds = l:iter():partition(
+                            function(e)
+          return e % 2 == 0
+        end)
+    assert.are.equal(evens, List {2, 4, 6, 8, 10})
+    assert.are.equal(odds, List {1, 3, 5, 7, 9})
+  end)
+end)
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)
diff --git a/tests/plenary/strings_spec.lua b/tests/plenary/strings_spec.lua
new file mode 100644
index 0000000..a528414
--- /dev/null
+++ b/tests/plenary/strings_spec.lua
@@ -0,0 +1,248 @@
+local strings = require('plenary.strings')
+local eq = assert.are.same
+
+describe('strings', function()
+  describe('strdisplaywidth', function()
+    for _, case in ipairs{
+      {str = 'abcde', expected = {single = 5, double = 5}},
+      -- This space below is a tab (U+0009)
+      {str = 'abc	de', expected = {single = 10, double = 10}},
+      {str = 'アイウエオ', expected = {single = 10, double = 10}},
+      {str = '├─┤', expected = {single = 3, double = 6}},
+      {str = 123, expected = {single = 3, double = 3}},
+    } do
+      for _, ambiwidth in ipairs{'single', 'double'} do
+        local item = type(case.str) == 'string' and '"%s"' or '%s'
+        local msg = ('ambiwidth = %s, '..item..' -> %d'):format(ambiwidth, case.str, case.expected[ambiwidth])
+        local original = vim.o.ambiwidth
+        vim.o.ambiwidth = ambiwidth
+        it('lua: '..msg, function()
+          eq(case.expected[ambiwidth], strings.strdisplaywidth(case.str))
+        end)
+        it('vim: '..msg, function()
+          eq(case.expected[ambiwidth], vim.fn.strdisplaywidth(case.str))
+        end)
+        vim.o.ambiwidth = original
+      end
+    end
+  end)
+
+  describe('strcharpart', function()
+    for _, case in ipairs{
+      {args = {'abcde', 2}, expected = 'cde'},
+      {args = {'abcde', 2, 2}, expected = 'cd'},
+      {args = {'アイウエオ', 2, 2}, expected = 'ウエ'},
+      {args = {'├───┤', 2, 2}, expected = '──'},
+    } do
+      local msg = ('("%s", %d, %s) -> "%s"'):format(case.args[1], case.args[2], tostring(case.args[3]), case.expected)
+      it('lua: '..msg, function()
+        eq(case.expected, strings.strcharpart(unpack(case.args)))
+      end)
+      it('vim: '..msg, function()
+        eq(case.expected, vim.fn.strcharpart(unpack(case.args)))
+      end)
+    end
+  end)
+
+  describe('truncate', function()
+    for _, case in ipairs{
+      {args = {'abcde', 6}, expected = {single = 'abcde', double = 'abcde'}},
+      {args = {'abcde', 5}, expected = {single = 'abcde', double = 'abcde'}},
+      {args = {'abcde', 4}, expected = {single = 'abc…', double = 'ab…'}},
+      {args = {'アイウエオ', 11}, expected = {single = 'アイウエオ', double = 'アイウエオ'}},
+      {args = {'アイウエオ', 10}, expected = {single = 'アイウエオ', double = 'アイウエオ'}},
+      {args = {'アイウエオ', 9}, expected = {single = 'アイウエ…', double = 'アイウ…'}},
+      {args = {'アイウエオ', 8}, expected = {single = 'アイウ…', double = 'アイウ…'}},
+      {args = {'├─┤', 7}, expected = {single = '├─┤', double = '├─┤'}},
+      {args = {'├─┤', 6}, expected = {single = '├─┤', double = '├─┤'}},
+      {args = {'├─┤', 5}, expected = {single = '├─┤', double = '├…'}},
+      {args = {'├─┤', 4}, expected = {single = '├─┤', double = '├…'}},
+      {args = {'├─┤', 3}, expected = {single = '├─┤', double = '…'}},
+      {args = {'├─┤', 2}, expected = {single = '├…', double = '…'}},
+    } do
+      for _, ambiwidth in ipairs{'single', 'double'} do
+        local msg = ('ambiwidth = %s, [%s, %d] -> %s'):format(
+          ambiwidth,
+          case.args[1],
+          case.args[2],
+          case.expected[ambiwidth]
+        )
+        it(msg, function()
+          local original = vim.o.ambiwidth
+          vim.o.ambiwidth = ambiwidth
+          eq(case.expected[ambiwidth], strings.truncate(unpack(case.args)))
+          vim.o.ambiwidth = original
+        end)
+      end
+    end
+  end)
+
+  describe('align_str', function()
+    for _, case in ipairs{
+      {args = {'abcde', 8}, expected = {single = 'abcde   ', double = 'abcde   '}},
+      {args = {'アイウ', 8}, expected = {single = 'アイウ  ', double = 'アイウ  '}},
+      {args = {'├─┤', 8}, expected = {single = '├─┤     ', double = '├─┤  '}},
+      {args = {'abcde', 8, true}, expected = {single = '   abcde', double = '   abcde'}},
+      {args = {'アイウ', 8, true}, expected = {single = '  アイウ', double = '  アイウ'}},
+      {args = {'├─┤', 8, true}, expected = {single = '     ├─┤', double = '  ├─┤'}},
+    } do
+      for _, ambiwidth in ipairs{'single', 'double'} do
+        local msg = ('ambiwidth = %s, [%s, %d, %s] -> "%s"'):format(
+          ambiwidth,
+          case.args[1],
+          case.args[2],
+          tostring(case.args[3]),
+          case.expected[ambiwidth]
+        )
+        it(msg, function()
+          local original = vim.o.ambiwidth
+          vim.o.ambiwidth = ambiwidth
+          eq(case.expected[ambiwidth], strings.align_str(unpack(case.args)))
+          vim.o.ambiwidth = original
+        end)
+      end
+    end
+  end)
+
+  describe('dedent', function()
+    local function lines(t)
+      return table.concat(t, '\n')
+    end
+    for _, case in ipairs{
+      {
+        msg = 'empty string',
+        tabstop = 8,
+        args = {''},
+        expected = '',
+      },
+      {
+        msg = 'in case tabs are longer than spaces',
+        tabstop = 8,
+        args = {
+          lines{
+            '		<Tab><Tab> -> 13 spaces',
+            '     5 spaces -> 0 space',
+          },
+        },
+        expected = lines{
+          '           <Tab><Tab> -> 13 spaces',
+          '5 spaces -> 0 space',
+        },
+      },
+      {
+        msg = 'in case tabs are shorter than spaces',
+        tabstop = 2,
+        args = {
+          lines{
+            '		<Tab><Tab> -> 0 space',
+            '     5spaces -> 1 space',
+          },
+        },
+        expected = lines{
+          '<Tab><Tab> -> 0 space',
+          ' 5spaces -> 1 space',
+        },
+      },
+      {
+        msg = 'ignores empty lines',
+        tabstop = 2,
+        args = {
+          lines{
+            '',
+            '',
+            '',
+            '        8 spaces -> 3 spaces',
+            '',
+            '',
+            '     5 spaces -> 0 space',
+            '',
+            '',
+            '',
+          },
+        },
+        expected = lines{
+          '',
+          '',
+          '',
+          '   8 spaces -> 3 spaces',
+          '',
+          '',
+          '5 spaces -> 0 space',
+          '',
+          '',
+          '',
+        },
+      },
+      {
+        msg = 'no indent',
+        tabstop = 2,
+        args = {
+          lines{
+            '	<Tab> -> 2 spaces',
+            'Here is no indent.',
+            '    4 spaces will remain',
+          },
+        },
+        expected = lines{
+          '  <Tab> -> 2 spaces',
+          'Here is no indent.',
+          '    4 spaces will remain',
+        },
+      },
+      {
+        msg = 'leave_indent = 4',
+        tabstop = 2,
+        args = {
+          lines{
+            '	<Tab> -> 6 spaces',
+            '0 indent -> 4 spaces',
+            '    4 spaces -> 8 spaces',
+          },
+          4,
+        },
+        expected = lines{
+          '      <Tab> -> 6 spaces',
+          '    0 indent -> 4 spaces',
+          '        4 spaces -> 8 spaces',
+        },
+      },
+      {
+        msg = 'typical usecase: <Tab> to 5 spaces',
+        tabstop = 4,
+        args = {
+          lines{
+            '',
+            '		Chapter 1',
+            '',
+            '	  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed',
+            '	do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+            '',
+            '	  Ut enim ad minim veniam, quis nostrud exercitation ullamco',
+            '	laboris nisi ut aliquip ex ea commodo consequat.',
+            '',
+          },
+          5,
+        },
+        expected = lines{
+          '',
+          '         Chapter 1',
+          '',
+          '       Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed',
+          '     do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+          '',
+          '       Ut enim ad minim veniam, quis nostrud exercitation ullamco',
+          '     laboris nisi ut aliquip ex ea commodo consequat.',
+          '',
+        },
+      },
+    } do
+      local msg = ('tabstop = %d, %s'):format(case.tabstop, case.msg)
+      it(msg, function()
+        local original = vim.bo.tabstop
+        vim.bo.tabstop = case.tabstop
+        eq(case.expected, strings.dedent(unpack(case.args)))
+        vim.bo.tabstop = original
+      end)
+    end
+  end)
+end)