Repository: nvim-lua/plenary.nvim
Pull request #134 (2021-04-12 15:49:17)

class abstraction from rxi/classic
from https://github.com/rxi/classic

tjdevries: I think I prefer `__super` over `super`. What do you think?

Patch:
diff --git a/README.md b/README.md
index 458b1ca..0aa8291 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ Plug 'nvim-lua/plenary.nvim'
 - `plenary.context_manager`
 - `plenary.test_harness`
 - `plenary.filetype`
-- `plenary.neorocks` (This may move to packer.nvim, but I have added some improvements to use it more as a library.)
+- `plenary.strings`
 
 ### plenary.job
 
@@ -96,7 +96,7 @@ nmap <leader>t <Plug>PlenaryTestFile
 To run a whole directory from the command line, you could do something like:
 
 ```
-nvim --headless -c 'PlenaryBustedDirectory tests/plenary/ {minimal_init = "tests/minimal_init.vim"}'
+nvim --headless -c "PlenaryBustedDirectory tests/plenary/ {minimal_init = 'tests/minimal_init.vim'}"
 ```
 
 Where the first argument is the directory you'd like to test. It will search for files with
@@ -180,65 +180,81 @@ return {
 }
 ```
 
-### plenary.popup
+### plenary.strings
 
-`popup_*` clone of Vim's commands. If it gets good enough, will submit PR to Neovim and write C wrappers
-to provide compatibility layer for Neovim.
+Re-implement VimL funcs to use them in Lua loop.
 
-Status: WIP
+* `strings.strdisplaywidth`
+* `strings.strcharpart`
 
-### plenary.window
+And some other funcs are here to deal with common problems.
 
-Window helper functions to wrap some of the more difficult cases. Particularly for floating windows.
+* `strings.truncate`
+* `strings.align_str`
+* `strings.dedent`
 
-Status: WIP
+### plenary.profile
 
-### Troubleshooting
+Thin wrapper around LuaJIT's [`jit.p` profiler](https://blast.hk/moonloader/luajit/ext_profiler.html).
 
-If you're having trouble / things are hanging / other problems:
+```lua
+require'plenary.profile'.start("profile.log")
 
+-- code to be profiled
+
+require'plenary.profile'.stop()
 ```
-$ export DEBUG_PLENARY=true
+
+You can use `start("profile.log", {flame = true})` to output the log in a
+flamegraph-compatible format. A flamegraph can be created from this using
+https://github.com/jonhoo/inferno via
 ```
+inferno-flamegraph profile.log > flame.svg
+```
+The resulting interactive SVG file can be viewed in any browser.
 
-This will enable debuggin for the plugin.
+Status: WIP
 
-### plenary.neorocks
+### plenary.popup
 
-Install lua packages with `luarocks`!
+`popup_*` clone of Vim's commands. If it gets good enough, will submit PR to Neovim and write C wrappers
+to provide compatibility layer for Neovim.
 
-Include the following somewhere in your configuration (either heredoc or sourced lua file):
+Status: WIP
+
+### plenary.window
+
+Window helper functions to wrap some of the more difficult cases. Particularly for floating windows.
+
+Status: WIP
+
+### plenary.collections
+
+Contains pure lua implementations for various standard collections.
 
 ```lua
-local neorocks = require("plenary.neorocks")
+local List = require 'plenary.collections.py_list'
 
--- Installs neorocks if necessary and then installs the package you need.
---  Is not synchronous, so should be called from Neovim's command line most likely.
-neorocks.install('penlight', 'pl')
+local myList = List { 9, 14, 32, 5 }
 
--- ensure_installed(package_name, lua_name, headless)
--- Only installs if currently not installed.
---
---   package_name : str             - Name of the package for luarocks
---   lua_name     : (optional) str  - Name of the package that you can require. Used to determine if we have it installed already (not from luarocks)
-neorocks.ensure_installed('penlight', 'pl')
-neorocks.ensure_installed('lua-cjson', 'cjson')
-```
+for i, v in myList:iter() do
+    print(i, v)
+end
 
-Inspiration: https://github.com/theHamsta/nvim_rocks . However, I've used quite a different end goal (following XDG_CONFIG standards, using `package.path` and `package.cpath` to load the packages and a different strategy of loading).
+```
 
+Status: WIP
 
-### And more to come :)
+### Troubleshooting
 
-- [ ] Floating window wrappers
-- [ ] Easy border windows to any floating window
+If you're having trouble / things are hanging / other problems:
 
+```
+$ export DEBUG_PLENARY=true
+```
 
+This will enable debuggin for the plugin.
 
-### Used by:
+### plenary.neorocks
 
-- [express_line.nvim](https://github.com/tjdevries/express_line.nvim)
-- [apyrori.nvim](https://github.com/tjdevries/apyrori.nvim)
-- [nlua.nvim](https://github.com/tjdevries/nlua.nvim)
-- (TODO) [popup.nvim](https://github.com/nvim-lua/popup.nvim)
-- (TODO) [telescope.nvim](https://github.com/nvim-lua/telescope.nvim)
+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 512b884..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',
@@ -31,6 +32,8 @@ return {
     ['jl'] = 'julia',
     ['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..dbec0b8
--- /dev/null
+++ b/lua/plenary/async/async.lua
@@ -0,0 +1,118 @@
+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(...)
+    local nargs = f.third(...)
+    assert(type(returned_function) == "function", "type error :: expected func")
+    local rstat, msg = pcall(returned_function, vararg.rotate(nargs, step, select(4, ...)))
+    if not rstat 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, argc)
+    assert(_PlenaryLeafTable[async_func] == nil, "Async function should not already be in the table")
+    _PlenaryLeafTable[async_func] = argc
+  end
+
+  function M.is_leaf_function(async_func)
+    return _PlenaryLeafTable[async_func] ~= nil
+  end
+
+  function M.get_leaf_function_argc(async_func)
+    return _PlenaryLeafTable[async_func]
+  end
+end
+
+---Creates an async function with a callback style function.
+---@param func function: A callback style function to be converted. The last argument must be the callback.
+---@param argc number: The number of arguments of func. Must be included.
+---@return function: Returns an async function
+M.wrap = function(func, argc)
+  if 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 nargs == argc then
+      return func(...)
+    else
+      return co.yield(func, argc, ...)
+    end
+  end
+
+  add_leaf_function(leaf, argc)
+
+  return leaf
+end
+
+---Use this to either run a future concurrently and then do something else
+---or use it to run a future with a callback in a non async context
+---@param async_function function
+---@param callback function
+M.run = function(async_function, callback)
+  if M.is_leaf_function(async_function) then
+    async_function(callback)
+  else
+    execute(async_function, callback)
+  end
+end
+
+---Use this to create a function which executes in an async context but
+---called from a non-async context. Inherently this cannot return anything
+---since it is non-blocking
+---@param func function
+M.void = function(func)
+  return function(...)
+    execute(func, nil, ...)
+  end
+end
+
+return M
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..bbb8e5c
--- /dev/null
+++ b/lua/plenary/async/util.lua
@@ -0,0 +1,131 @@
+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, ...)
+  local nargs = a.get_leaf_function_argc(async_fn)
+  if nargs then
+    local tx, rx = channel.oneshot()
+    local stat, ret = pcall(async_fn, vararg.rotate(nargs, tx, ...))
+    if not stat then
+      return stat, ret
+    else
+      return stat, rx()
+    end
+  else
+    return pcall(async_fn, ...)
+  end
+end
+
+function M.protected(async_fn)
+  return function()
+    return M.apcall(async_fn)
+  end
+end
+
+---An async function that when called will yield to the neovim scheduler to be able to call the api.
+M.scheduler = a.wrap(vim.schedule, 1)
+
+return M
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/async_lib/async.lua b/lua/plenary/async_lib/async.lua
index 3fce4a5..2ff047d 100644
--- a/lua/plenary/async_lib/async.lua
+++ b/lua/plenary/async_lib/async.lua
@@ -175,6 +175,10 @@ M.void = function(async_func)
   end
 end
 
+M.async_void = function(func)
+  return M.void(M.async(func))
+end
+
 ---creates an async function
 ---@param func function
 ---@return function: returns an async function
diff --git a/lua/plenary/async_lib/lsp.lua b/lua/plenary/async_lib/lsp.lua
index 6f96354..237cc52 100644
--- a/lua/plenary/async_lib/lsp.lua
+++ b/lua/plenary/async_lib/lsp.lua
@@ -2,7 +2,14 @@ local a = require('plenary.async_lib.async')
 
 local M = {}
 
----Same as vim.lsp.buf_request but works with async await
+---This will be deprecated because the callback can be called multiple times.
+---This will give a coroutine error because the coroutine will be resumed multiple times.
+---Please use buf_request_all instead.
 M.buf_request = a.wrap(vim.lsp.buf_request, 4)
 
+--This was recently merged into master so we just check if it is there
+if vim.lsp.buf_request_all ~= nil then
+  M.buf_request_all = a.wrap(vim.lsp.buf_request_all, 4)
+end
+
 return M
diff --git a/lua/plenary/class.lua b/lua/plenary/class.lua
new file mode 100644
index 0000000..e06808c
--- /dev/null
+++ b/lua/plenary/class.lua
@@ -0,0 +1,81 @@
+---@brief [[
+---classic
+---
+---Copyright (c) 2014, rxi
+---@brief ]]
+
+---@class Object
+local Object = {}
+Object.__index = Object
+
+---Does nothing.
+---You have to implement this yourself for extra functionality when initializing
+---@param self Object
+function Object:new()
+end
+
+---Create a new class/object by extending the base Object class.
+---The extended object will have a field called `super` that will access the super class.
+---@param self Object
+---@return Object
+function Object:extend()
+  local cls = {}
+  for k, v in pairs(self) do
+    if k:find("__") == 1 then
+      cls[k] = v
+    end
+  end
+  cls.__index = cls
+  cls.super = self
+  setmetatable(cls, self)
+  return cls
+end
+
+---Implement a mixin onto this Object.
+---@param self Object
+---@param nil ...
+function Object:implement(...)
+  for _, cls in pairs({...}) do
+    for k, v in pairs(cls) do
+      if self[k] == nil and type(v) == "function" then
+        self[k] = v
+      end
+    end
+  end
+end
+
+---Checks if the object is an instance
+---This will start with the lowest class and loop over all the superclasses.
+---@param self Object
+---@param T Object
+---@return boolean
+function Object:is(T)
+  local mt = getmetatable(self)
+  while mt do
+    if mt == T then
+      return true
+    end
+    mt = getmetatable(mt)
+  end
+  return false
+end
+
+---The default tostring implementation for an object.
+---You can override this to provide a different tostring.
+---@param self Object
+---@return string
+function Object:__tostring()
+  return "Object"
+end
+
+---You can call the class the initialize it without using `Object:new`.
+---@param self Object
+---@param nil ...
+---@return Object
+function Object:__call(...)
+  local obj = setmetatable({}, self)
+  obj:new(...)
+  return obj
+end
+
+return Object
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/functional.lua b/lua/plenary/functional.lua
index f417ce7..9618e8e 100644
--- a/lua/plenary/functional.lua
+++ b/lua/plenary/functional.lua
@@ -1,14 +1,5 @@
 local f = {}
 
-function f.map(fun, iter)
-  local results = {}
-  for _, v in pairs(iter) do
-    table.insert(results, fun(v))
-  end
-
-  return results
-end
-
 function f.kv_pairs(t)
   local results = {}
   for k, v in pairs(t) do
@@ -32,9 +23,9 @@ function f.partial(fun, ...)
   end
 end
 
-function f.any(f, iterable)
+function f.any(fun, iterable)
   for k, v in pairs(iterable) do
-    if f(k, v) then
+    if fun(k, v) then
       return true
     end
   end
@@ -42,9 +33,9 @@ function f.any(f, iterable)
   return false
 end
 
-function f.all(f, iterable)
+function f.all(fun, iterable)
   for k, v in pairs(iterable) do
-    if not f(k, v) then
+    if not fun(k, v) then
       return false
     end
   end
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/log.lua b/lua/plenary/log.lua
index e10e963..3ebf10e 100644
--- a/lua/plenary/log.lua
+++ b/lua/plenary/log.lua
@@ -18,7 +18,8 @@ local default_config = {
   plugin = 'plenary',
 
   -- Should print the output to neovim while running
-  use_console = true,
+  -- values: 'sync','async',false
+  use_console = 'async',
 
   -- Should highlighting be used in console (using echohl)
   highlights = true,
@@ -51,7 +52,7 @@ local unpack = unpack or table.unpack
 log.new = function(config, standalone)
   config = vim.tbl_deep_extend("force", default_config, config)
 
-  local outfile = string.format('%s/%s.log', vim.api.nvim_call_function('stdpath', {'data'}), config.plugin)
+  local outfile = string.format('%s/%s.log', vim.api.nvim_call_function('stdpath', {'cache'}), config.plugin)
 
   local obj
   if standalone then
@@ -103,7 +104,7 @@ log.new = function(config, standalone)
 
     -- Output to console
     if config.use_console then
-      vim.schedule(function()
+      local log_to_console = function()
         local console_string = string.format(
           "[%-6s%s] %s: %s",
           nameupper,
@@ -129,7 +130,12 @@ log.new = function(config, standalone)
         if config.highlights and level_config.hl then
           vim.cmd "echohl NONE"
         end
-      end)
+      end
+      if config.use_console == 'sync' and not vim.in_fast_event() then
+        log_to_console()
+      else
+        vim.schedule(log_to_console)
+      end
     end
 
     -- Output to log file
@@ -151,7 +157,7 @@ log.new = function(config, standalone)
     end
 
     -- log.fmt_info("These are %s strings", "formatted")
-    obj[("fmt_%s" ):format(x.name)] = function()
+    obj[("fmt_%s" ):format(x.name)] = function(...)
       return log_at_level(i, x, function(...)
         local passed = {...}
         local fmt = table.remove(passed, 1)
@@ -160,7 +166,7 @@ log.new = function(config, standalone)
           table.insert(inspected, vim.inspect(v))
         end
         return string.format(fmt, unpack(inspected))
-      end)
+      end, ...)
     end
 
     -- log.lazy_info(expensive_to_calculate)
diff --git a/lua/plenary/neorocks/init.lua b/lua/plenary/neorocks/init.lua
index 7e1785a..a29f53c 100644
--- a/lua/plenary/neorocks/init.lua
+++ b/lua/plenary/neorocks/init.lua
@@ -1,430 +1,1 @@
-local Job = require('plenary.job')
-local Path = require('plenary.path')
-
-local log = require('plenary.log')
-local run = require('plenary.run')
-local window_float = require('plenary.window.float')
-
--- TODO: We should consider not making windows when headless.
-local headless = require('plenary.nvim_meta').is_headless
-
-local neorocks = {}
-
-neorocks.scheduler = require('plenary.neorocks.scheduler'):new()
-
-neorocks.job_with_display_output = function(title_text, command, args)
-  log.debug("Starting job:", title_text, command, args)
-
-  if type(title_text) == "string" then
-    title_text = {title_text}
-  end
-
-  if headless then
-    io.write(command .. '\n')
-    io.write(vim.inspect(args) .. '\n')
-  end
-
-  local outputter = vim.schedule_wrap(function(_, data, self)
-    if data == nil then
-      return
-    end
-
-    if not self then
-      return
-    end
-
-    if headless then
-      io.write(data .. '\n')
-      return
-    end
-
-    local bufnr = self.user_data.views.bufnr
-    local win_id = self.user_data.views.win_id
-
-    if not vim.api.nvim_buf_is_valid(bufnr) then
-      return
-    end
-
-    local split_data = vim.split(data, "\n")
-    if #split_data > 1 and split_data[#split_data] == "" then
-      split_data[#split_data] = nil
-    end
-
-    vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, split_data)
-
-    if not vim.api.nvim_win_is_valid(win_id) then
-      return
-    end
-
-    local final_row = #vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
-    vim.api.nvim_win_set_cursor(win_id, {final_row, 0})
-  end)
-
-  return Job:new {
-    -- TODO: Should test if this splits or not... otherwise gotta do that annoyin thing
-    command = command,
-    args = args,
-
-    on_start = function(self)
-      self.user_data.views = window_float.centered_with_top_win(title_text, {winblend = 0})
-
-      vim.fn.win_gotoid(self.user_data.views.win_id)
-      vim.api.nvim_win_set_option(self.user_data.views.win_id, 'wrap', false)
-    end,
-
-    on_stdout = outputter,
-    on_stderr = outputter,
-
-    on_exit = vim.schedule_wrap(function(self, code)
-      local bufnr = self.user_data.views.bufnr
-      if not vim.api.nvim_buf_is_valid(bufnr) then return end
-
-      if code == 0 then
-        vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, {"", ("="):rep(40), "  Success! <Enter> to close.", ("="):rep(40)})
-      else
-        vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, {"", ("="):rep(40), "  Failed to complete. <Enter> to close.", ("="):rep(40)})
-      end
-
-      local win_id = self.user_data.views.win_id
-      if not vim.api.nvim_win_is_valid(win_id) then return end
-
-      local final_row = #vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
-      vim.api.nvim_win_set_cursor(win_id, {final_row, 0})
-
-      vim.api.nvim_buf_set_keymap(bufnr, 'n', '<CR>', ':quit<CR>', {silent = true, noremap = true})
-    end)
-  }
-end
-
----@return table Of the form: {
----     lua: Lua Version (5.1, 5.2, etc.)
----     jit: Jit Version (2.1.0-beta3, or nil)
----     dir: Directory of hererocks installation
---- }
-neorocks._lua_version = (function()
-  if jit then
-    return {
-      lua = string.gsub(_VERSION, "Lua ", ""),
-      jit = string.gsub(jit.version, "LuaJIT ", ""),
-      dir = string.gsub(jit.version, "LuaJIT ", "")
-    }
-  end
-
-  error("NEOROCKS: Unsupported Lua Versions", _VERSION)
-end)()
-
-neorocks._base_path                  = Path:new(vim.fn.stdpath('cache'), 'plenary_hererocks')
-neorocks._hererocks_file             = Path:new(vim.fn.stdpath('cache'), 'hererocks.py')
-neorocks._hererocks_install_location = Path:new(neorocks._base_path, neorocks._lua_version.dir)
-neorocks._is_setup                   = vim.fn.isdirectory(Path:new(neorocks._hererocks_install_location, "lib"):absolute()) > 0
-
-neorocks._get_hererocks_job = function()
-  local url_loc = 'https://raw.githubusercontent.com/luarocks/hererocks/latest/hererocks.py'
-
-  local command, args
-  if vim.fn.executable('curl') > 0 then
-    command = 'curl'
-    args = {url_loc, '-o', neorocks._hererocks_file:absolute()}
-  elseif vim.fn.executable('wget') > 0 then
-    command = 'wget'
-    args = {url_loc, '-O', neorocks._hererocks_file:absolute(), '--verbose'}
-  else
-    error('"curl" or "wget" is required')
-  end
-
-  return neorocks.job_with_display_output(
-    {"Installing hererocks"},
-    command,
-    args
-  )
-end
-
-neorocks._get_setup_job = function(force, opts)
-  local lua_version = neorocks._lua_version
-  local install_location = neorocks._hererocks_install_location
-
-  if force == nil then
-    force = false
-  end
-
-  if opts == nil then
-    opts = {}
-    opts.split = true
-    opts.wait = true
-  end
-
-  if neorocks._is_setup and not force then
-    return
-  end
-
-  if vim.fn.filereadable(neorocks._hererocks_file:absolute()) == 0 then
-    neorocks.scheduler:insert(neorocks._get_hererocks_job(opts))
-  end
-
-  if lua_version.jit then
-    return neorocks.job_with_display_output(
-      {"Installing luajit & luarocks"},
-      -- TODO: This should be a user specified python, just in case?
-      "python",
-      {
-        neorocks._hererocks_file:absolute(),
-        "--verbose",
-        force and "-i" or "",
-        "-j",
-        lua_version.jit,
-        "-r",
-        "latest",
-        install_location:absolute()
-      }
-    )
-  else
-    error("Unsupported version")
-  end
-end
-
-neorocks.setup = function(force, quit)
-  vim.fn.mkdir(neorocks._base_path:absolute(), "p")
-  quit = (quit == nil and true) or quit
-  neorocks.scheduler:insert(neorocks._get_setup_job(force))
-
-  if quit then
-    neorocks.scheduler:insert {
-      start = function()
-        vim.cmd [[qa!]]
-      end,
-
-      add_on_exit_callback = function()
-      end,
-    }
-  end
-end
-
-neorocks._get_package_paths = function()
-  local lua_version = neorocks._lua_version
-  local install_location = neorocks._hererocks_install_location
-
-  local install_path = install_location:joinpath(
-    "lib",
-    "luarocks",
-    string.format("rocks-%s", lua_version.lua)
-  )
-
-  local share_path = install_location:joinpath(
-    "share",
-    "lua",
-    lua_version.lua
-  )
-
-  local gen_pattern = function(directory)
-    return string.format(
-    "%s/?.lua;%s/?/init.lua",
-    directory,
-    directory
-  )
-  end
-
-  return gen_pattern(share_path:absolute()) .. ';' .. gen_pattern(install_path:absolute())
-end
-
---- Idempotent adding of paths for both package.path and package.cpath
-neorocks.setup_paths = function()
-  if not neorocks._is_setup then
-    return
-  end
-
-  if neorocks._path_setup_complete then
-    return
-  end
-
-  local lua_version = neorocks._lua_version
-  local install_location = neorocks._hererocks_install_location
-
-  local match_install_path = neorocks._get_package_paths()
-
-  if not string.find(package.path, match_install_path, 1, true) then
-    package.path = package.path .. ';' .. match_install_path
-  end
-
-  local install_cpath = install_location:joinpath("lib", "lua", lua_version.lua)
-  local match_install_cpath = string.format(
-    "%s/?.so",
-    install_cpath:absolute()
-  )
-  if not string.find(package.cpath, match_install_cpath, 1, true) then
-    package.cpath = package.cpath .. ';' .. match_install_cpath
-  end
-
-  neorocks._path_setup_complete = true
-end
-
--- activate hererocks based on current $SHELL
-local function source_activate(install_location, activate_file)
-  return string.format('source %s', install_location:joinpath('bin', activate_file):absolute())
-end
-
---- Get the string to source hererocks
-neorocks._source_string = function(install_location)
-  local user_shell = os.getenv("SHELL") or "bash"
-  local shell = user_shell:gmatch("([^/]*)$")()
-  if shell == "fish" then
-    return source_activate(install_location, 'activate.fish')
-  elseif shell == "csh" then
-    return source_activate(install_location, 'activate.csh')
-  end
-  return source_activate(install_location, 'activate')
-end
-
-neorocks._luarocks_exec = function(luarocks_arg)
-  local install_location = neorocks._hererocks_install_location
-
-  return vim.fn.systemlist(string.format(
-    "%s && luarocks %s",
-    neorocks._source_string(install_location),
-    luarocks_arg
-  ))
-end
-
---- Properly source and run a luarocks command. Will run `luarocks $luarocks_arg`
-neorocks._luarocks_run = function(luarocks_arg)
-  local install_location = neorocks._hererocks_install_location
-  local source_string = neorocks._source_string(install_location)
-
-  run.with_displayed_output(
-    {"Lua rocks install"},
-    string.format(
-      '%s && luarocks %s',
-      source_string,
-      luarocks_arg
-    ),
-    {
-      split = true,
-      wait = true
-    }
-  )
-end
-
---- Properly source and return the output of a luarocks command.
-neorocks._get_luarocks_job = function(luarocks_arg)
-  local install_location = neorocks._hererocks_install_location
-  local source_string = neorocks._source_string(install_location)
-
-  local command = "bash"
-  local args = {"-c", string.format('%s && luarocks %s', source_string, luarocks_arg)}
-
-  return neorocks.job_with_display_output("Luarocks: " .. luarocks_arg, command, args)
-end
-
-
-neorocks._get_install_job = function(package_name)
-  return neorocks._get_luarocks_job(string.format('install %s', package_name))
-end
-
-neorocks.install = function(package_name, lua_name, force, should_quit)
-  neorocks.scheduler:insert(neorocks._get_setup_job())
-
-  if not force and neorocks.is_package_installed(package_name, lua_name) then
-    print(package_name, 'is already installed!')
-    return
-  end
-
-  local install_job = neorocks.scheduler:insert(neorocks._get_install_job(package_name))
-
-  if headless and should_quit == nil then
-    should_quit = true
-  end
-
-  if should_quit then
-    neorocks.scheduler:insert {
-      start = function()
-        vim.cmd [[qa!]]
-      end,
-
-      add_on_exit_callback = function()
-      end,
-    }
-  end
-
-  return install_job
-end
-
-neorocks.ensure_installed = function(package_name, lua_name)
-  -- Don't try and install on startup. It's annoying.
-  -- Maybe someday we can do it.
-  if not neorocks._is_setup then
-    vim.api.nvim_err_writeln("Neorocks is not yet set up. Please manually install")
-    return
-  end
-
-  neorocks.setup_paths()
-
-  if lua_name == nil then
-    lua_name = package_name
-  end
-
-  if neorocks.is_package_installed(package_name, lua_name) then
-    return
-  end
-
-  neorocks.install(package_name, lua_name):wait(60000, 100, true)
-end
-
-neorocks.remove = function(package_name)
-  neorocks._luarocks_run(string.format('remove %s', package_name))
-end
-
-neorocks.list = function(package_filter)
-  if package_filter == nil then
-    package_filter = ''
-  end
-
-  local result = {}
-  for _, line in ipairs(
-    neorocks._luarocks_exec(
-      string.format('list %s --porcelain', package_filter),
-      true
-    )
-  ) do
-    for l_package, version, status, install_path in string.gmatch(line, "([^\t]+)\t([^\t]+)\t([^\t]+)\t([^\t]+)") do
-      table.insert(result, {
-        l_package = l_package,
-        version = version,
-        status = status,
-        install_path = install_path
-      })
-    end
-  end
-
-  return result
-end
-
-neorocks.is_package_installed = function(package_name, lua_name)
-  -- TODO: Decided if I need anything more than this.
-  -- This tells me if it's available or not, which is really all I care about.
-  if lua_name == nil then
-    lua_name = package_name
-  end
-
-  if package.loaded[package_name] then
-    return true
-  end
-
-  local can_import, _ = pcall(function() return require(lua_name) end)
-  if can_import then
-    return true
-  end
-
-  local options = neorocks.list(package_name)
-
-  for _, p in ipairs(options) do
-    if p.l_package == package_name then
-      return true
-    end
-  end
-
-  return false
-end
-
--- package.searchpath(${1:name: string}, ${2:path: string}, ${3:sep: string}, ${4:rep: string})
--- package.searchpath("pl", package.path)
-
-return neorocks
+error("neorocks is no longer supported. Please use packer.nvim or other project for neorocks usage.")
diff --git a/lua/plenary/neorocks/scheduler.lua b/lua/plenary/neorocks/scheduler.lua
deleted file mode 100644
index 4127e71..0000000
--- a/lua/plenary/neorocks/scheduler.lua
+++ /dev/null
@@ -1,44 +0,0 @@
-local scheduler = {}
-
-function scheduler:new()
-  return setmetatable({
-    items = {},
-
-    in_progress = nil,
-  }, {
-    __index = self,
-  })
-end
-
-function scheduler:insert(item)
-  if not item then
-    return item
-  end
-
-  table.insert(self.items, item)
-
-  if not self.in_progress then
-    self:_run_item(item)
-  else
-    self:_chain_item(item)
-  end
-
-  return item
-end
-
-function scheduler:_run_item(item)
-  self.in_progress = true
-  item:add_on_exit_callback(function()
-    self.in_progress = false
-  end)
-
-  item:start()
-end
-
-function scheduler:_chain_item(item)
-  self.items[#self.items - 1]:add_on_exit_callback(vim.schedule_wrap(function()
-    self:_run_item(item)
-  end))
-end
-
-return scheduler
diff --git a/lua/plenary/path.lua b/lua/plenary/path.lua
index 8d7376d..cf3d907 100644
--- a/lua/plenary/path.lua
+++ b/lua/plenary/path.lua
@@ -7,8 +7,6 @@ local uv = vim.loop
 
 local F = require('plenary.functional')
 
-
-
 local S_IF = {
   -- S_IFDIR  = 0o040000  # directory
   DIR = 0x4000,
@@ -59,6 +57,42 @@ local function is_root(pathname)
   return pathname == '/'
 end
 
+local _split_by_separator = (function()
+    local formatted =  string.format("([^%s]+)", path.sep)
+    return function(filepath)
+        local t = {}
+        for str in string.gmatch(filepath, formatted) do
+            table.insert(t, str)
+        end
+        return t
+    end
+end)()
+
+local function _normalize_path(filename)
+  local out_file = filename
+
+  local has = string.find(filename, "..", 1, true)
+
+  if has then
+      local parts = _split_by_separator(filename)
+
+      local idx = 1
+      repeat
+          if parts[idx] == ".." then
+              table.remove(parts, idx)
+              table.remove(parts, idx - 1)
+              idx = idx - 2
+          end
+          idx = idx + 1
+      until idx > #parts
+
+      out_file = path.root(filename) .. table.concat(parts, path.sep)
+  end
+
+  return out_file
+end
+
+
 local clean = function(pathname)
   -- Remove double path seps, it's annoying
   pathname = pathname:gsub(path.sep .. path.sep, path.sep)
@@ -206,9 +240,9 @@ end
 
 function Path:absolute()
   if self:is_absolute() then
-    return self.filename
+    return _normalize_path(self.filename)
   else
-    return self._absolute or table.concat({self._cwd, self.filename}, self._sep)
+    return _normalize_path(self._absolute or table.concat({self._cwd, self.filename}, self._sep))
   end
 end
 
@@ -243,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
 
@@ -266,11 +298,32 @@ function Path:normalize(cwd)
   -- Substitute home directory w/ "~"
   self.filename = self.filename:gsub("^" .. path.home, '~', 1)
 
-  return self.filename
+  return _normalize_path(self.filename)
+end
+
+local function shorten_len(filename, len)
+  local final_match
+  local final_path_components = {}
+  for match in (filename..path.sep):gmatch("(.-)"..path.sep) do
+    if #match > len then
+      table.insert(final_path_components, string.sub(match, 1, len))
+    else
+      table.insert(final_path_components, match)
+    end
+    table.insert(final_path_components, path.sep)
+    final_match = match
+  end
+
+  local l = #final_path_components -- so that we don't need to keep calculating length
+  table.remove(final_path_components, l) -- remove final slash
+  table.remove(final_path_components, l-1) -- remvove shortened final component
+  table.insert(final_path_components, final_match) -- insert full final component
+
+  return table.concat(final_path_components)
 end
 
 local shorten = (function()
-  if jit then
+  if jit and path.sep ~= '\\' then
     local ffi = require('ffi') ffi.cdef [[
     typedef unsigned char char_u;
     char_u *shorten_dir(char_u *str);
@@ -286,11 +339,15 @@ local shorten = (function()
     end
   end
   return function(filename)
-    return filename
+    return shorten_len(filename, 1)
   end
 end)()
 
-function Path:shorten()
+function Path:shorten(len)
+  assert(len ~= 0, 'len must be at least 1')
+  if len and len > 1 then
+    return shorten_len(self.filename, len)
+  end
   return shorten(self.filename)
 end
 
@@ -554,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
@@ -588,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/profile.lua b/lua/plenary/profile.lua
index 6c8ad72..51c4b05 100644
--- a/lua/plenary/profile.lua
+++ b/lua/plenary/profile.lua
@@ -1,5 +1,25 @@
 local profile = {}
 
+-- bundled version of upstream jit.p until LuaJIT is updated to include
+-- https://github.com/LuaJIT/LuaJIT/commit/95140c50010c0557af66dac944403a1a65dd312c
+local p = require'plenary.profile.p'
+
+---start profiling using LuaJIT profiler
+---@param out name and path of log file
+---@param opts table of options
+---            flame (bool, default false) write log in flamegraph format
+--                   (see https://github.com/jonhoo/inferno)
+function profile.start(out, opts)
+    out = out or "profile.log"
+    opts = opts or {}
+    popts = "10,i1,s,m0"
+    if opts.flame then popts = popts .. ",G" end
+    p.start(popts, out)
+end
+
+---stop profiling
+profile.stop = p.stop
+
 function profile.benchmark(iterations, f, ...)
   local start_time = vim.loop.hrtime()
   for _ = 1, iterations do
diff --git a/lua/plenary/profile/p.lua b/lua/plenary/profile/p.lua
new file mode 100644
index 0000000..c9ec1d8
--- /dev/null
+++ b/lua/plenary/profile/p.lua
@@ -0,0 +1,312 @@
+----------------------------------------------------------------------------
+-- LuaJIT profiler.
+--
+-- Copyright (C) 2005-2021 Mike Pall. All rights reserved.
+-- Released under the MIT license. See Copyright Notice in luajit.h
+----------------------------------------------------------------------------
+--
+-- This module is a simple command line interface to the built-in
+-- low-overhead profiler of LuaJIT.
+--
+-- The lower-level API of the profiler is accessible via the "jit.profile"
+-- module or the luaJIT_profile_* C API.
+--
+-- Example usage:
+--
+--   luajit -jp myapp.lua
+--   luajit -jp=s myapp.lua
+--   luajit -jp=-s myapp.lua
+--   luajit -jp=vl myapp.lua
+--   luajit -jp=G,profile.txt myapp.lua
+--
+-- The following dump features are available:
+--
+--   f  Stack dump: function name, otherwise module:line. Default mode.
+--   F  Stack dump: ditto, but always prepend module.
+--   l  Stack dump: module:line.
+--   <number> stack dump depth (callee < caller). Default: 1.
+--   -<number> Inverse stack dump depth (caller > callee).
+--   s  Split stack dump after first stack level. Implies abs(depth) >= 2.
+--   p  Show full path for module names.
+--   v  Show VM states. Can be combined with stack dumps, e.g. vf or fv.
+--   z  Show zones. Can be combined with stack dumps, e.g. zf or fz.
+--   r  Show raw sample counts. Default: show percentages.
+--   a  Annotate excerpts from source code files.
+--   A  Annotate complete source code files.
+--   G  Produce raw output suitable for graphical tools (e.g. flame graphs).
+--   m<number> Minimum sample percentage to be shown. Default: 3.
+--   i<number> Sampling interval in milliseconds. Default: 10.
+--
+----------------------------------------------------------------------------
+
+-- Cache some library functions and objects.
+local jit = require("jit")
+assert(jit.version_num == 20100, "LuaJIT core/library version mismatch")
+local profile = require("jit.profile")
+local vmdef = require("jit.vmdef")
+local math = math
+local pairs, ipairs, tonumber, floor = pairs, ipairs, tonumber, math.floor
+local sort, format = table.sort, string.format
+local stdout = io.stdout
+local zone -- Load jit.zone module on demand.
+
+-- Output file handle.
+local out
+
+------------------------------------------------------------------------------
+
+local prof_ud
+local prof_states, prof_split, prof_min, prof_raw, prof_fmt, prof_depth
+local prof_ann, prof_count1, prof_count2, prof_samples
+
+local map_vmmode = {
+  N = "Compiled",
+  I = "Interpreted",
+  C = "C code",
+  G = "Garbage Collector",
+  J = "JIT Compiler",
+}
+
+-- Profiler callback.
+local function prof_cb(th, samples, vmmode)
+  prof_samples = prof_samples + samples
+  local key_stack, key_stack2, key_state
+  -- Collect keys for sample.
+  if prof_states then
+    if prof_states == "v" then
+      key_state = map_vmmode[vmmode] or vmmode
+    else
+      key_state = zone:get() or "(none)"
+    end
+  end
+  if prof_fmt then
+    key_stack = profile.dumpstack(th, prof_fmt, prof_depth)
+    key_stack = key_stack:gsub("%[builtin#(%d+)%]", function(x)
+      return vmdef.ffnames[tonumber(x)]
+    end)
+    if prof_split == 2 then
+      local k1, k2 = key_stack:match("(.-) [<>] (.*)")
+      if k2 then key_stack, key_stack2 = k1, k2 end
+    elseif prof_split == 3 then
+      key_stack2 = profile.dumpstack(th, "l", 1)
+    end
+  end
+  -- Order keys.
+  local k1, k2
+  if prof_split == 1 then
+    if key_state then
+      k1 = key_state
+      if key_stack then k2 = key_stack end
+    end
+  elseif key_stack then
+    k1 = key_stack
+    if key_stack2 then k2 = key_stack2 elseif key_state then k2 = key_state end
+  end
+  -- Coalesce samples in one or two levels.
+  if k1 then
+    local t1 = prof_count1
+    t1[k1] = (t1[k1] or 0) + samples
+    if k2 then
+      local t2 = prof_count2
+      local t3 = t2[k1]
+      if not t3 then t3 = {}; t2[k1] = t3 end
+      t3[k2] = (t3[k2] or 0) + samples
+    end
+  end
+end
+
+------------------------------------------------------------------------------
+
+-- Show top N list.
+local function prof_top(count1, count2, samples, indent)
+  local t, n = {}, 0
+  for k in pairs(count1) do
+    n = n + 1
+    t[n] = k
+  end
+  sort(t, function(a, b) return count1[a] > count1[b] end)
+  for i=1,n do
+    local k = t[i]
+    local v = count1[k]
+    local pct = floor(v*100/samples + 0.5)
+    if pct < prof_min then break end
+    if not prof_raw then
+      out:write(format("%s%2d%%  %s\n", indent, pct, k))
+    elseif prof_raw == "r" then
+      out:write(format("%s%5d  %s\n", indent, v, k))
+    else
+      out:write(format("%s %d\n", k, v))
+    end
+    if count2 then
+      local r = count2[k]
+      if r then
+	prof_top(r, nil, v, (prof_split == 3 or prof_split == 1) and "  -- " or
+			    (prof_depth < 0 and "  -> " or "  <- "))
+      end
+    end
+  end
+end
+
+-- Annotate source code
+local function prof_annotate(count1, samples)
+  local files = {}
+  local ms = 0
+  for k, v in pairs(count1) do
+    local pct = floor(v*100/samples + 0.5)
+    ms = math.max(ms, v)
+    if pct >= prof_min then
+      local file, line = k:match("^(.*):(%d+)$")
+      if not file then file = k; line = 0 end
+      local fl = files[file]
+      if not fl then fl = {}; files[file] = fl; files[#files+1] = file end
+      line = tonumber(line)
+      fl[line] = prof_raw and v or pct
+    end
+  end
+  sort(files)
+  local fmtv, fmtn = " %3d%% | %s\n", "      | %s\n"
+  if prof_raw then
+    local n = math.max(5, math.ceil(math.log10(ms)))
+    fmtv = "%"..n.."d | %s\n"
+    fmtn = (" "):rep(n).." | %s\n"
+  end
+  local ann = prof_ann
+  for _, file in ipairs(files) do
+    local f0 = file:byte()
+    if f0 == 40 or f0 == 91 then
+      out:write(format("\n====== %s ======\n[Cannot annotate non-file]\n", file))
+      break
+    end
+    local fp, err = io.open(file)
+    if not fp then
+      out:write(format("====== ERROR: %s: %s\n", file, err))
+      break
+    end
+    out:write(format("\n====== %s ======\n", file))
+    local fl = files[file]
+    local n, show = 1, false
+    if ann ~= 0 then
+      for i=1,ann do
+	if fl[i] then show = true; out:write("@@ 1 @@\n"); break end
+      end
+    end
+    for line in fp:lines() do
+      if line:byte() == 27 then
+	out:write("[Cannot annotate bytecode file]\n")
+	break
+      end
+      local v = fl[n]
+      if ann ~= 0 then
+	local v2 = fl[n+ann]
+	if show then
+	  if v2 then show = n+ann elseif v then show = n
+	  elseif show+ann < n then show = false end
+	elseif v2 then
+	  show = n+ann
+	  out:write(format("@@ %d @@\n", n))
+	end
+	if not show then goto next end
+      end
+      if v then
+	out:write(format(fmtv, v, line))
+      else
+	out:write(format(fmtn, line))
+      end
+    ::next::
+      n = n + 1
+    end
+    fp:close()
+  end
+end
+
+------------------------------------------------------------------------------
+
+-- Finish profiling and dump result.
+local function prof_finish()
+  if prof_ud then
+    profile.stop()
+    local samples = prof_samples
+    if samples == 0 then
+      if prof_raw ~= true then out:write("[No samples collected]\n") end
+      return
+    end
+    if prof_ann then
+      prof_annotate(prof_count1, samples)
+    else
+      prof_top(prof_count1, prof_count2, samples, "")
+    end
+    prof_count1 = nil
+    prof_count2 = nil
+    prof_ud = nil
+    if out ~= stdout then out:close() end
+  end
+end
+
+-- Start profiling.
+local function prof_start(mode)
+  local interval = ""
+  mode = mode:gsub("i%d*", function(s) interval = s; return "" end)
+  prof_min = 3
+  mode = mode:gsub("m(%d+)", function(s) prof_min = tonumber(s); return "" end)
+  prof_depth = 1
+  mode = mode:gsub("%-?%d+", function(s) prof_depth = tonumber(s); return "" end)
+  local m = {}
+  for c in mode:gmatch(".") do m[c] = c end
+  prof_states = m.z or m.v
+  if prof_states == "z" then zone = require("jit.zone") end
+  local scope = m.l or m.f or m.F or (prof_states and "" or "f")
+  local flags = (m.p or "")
+  prof_raw = m.r
+  if m.s then
+    prof_split = 2
+    if prof_depth == -1 or m["-"] then prof_depth = -2
+    elseif prof_depth == 1 then prof_depth = 2 end
+  elseif mode:find("[fF].*l") then
+    scope = "l"
+    prof_split = 3
+  else
+    prof_split = (scope == "" or mode:find("[zv].*[lfF]")) and 1 or 0
+  end
+  prof_ann = m.A and 0 or (m.a and 3)
+  if prof_ann then
+    scope = "l"
+    prof_fmt = "pl"
+    prof_split = 0
+    prof_depth = 1
+  elseif m.G and scope ~= "" then
+    prof_fmt = flags..scope.."Z;"
+    prof_depth = -100
+    prof_raw = true
+    prof_min = 0
+  elseif scope == "" then
+    prof_fmt = false
+  else
+    local sc = prof_split == 3 and m.f or m.F or scope
+    prof_fmt = flags..sc..(prof_depth >= 0 and "Z < " or "Z > ")
+  end
+  prof_count1 = {}
+  prof_count2 = {}
+  prof_samples = 0
+  profile.start(scope:lower()..interval, prof_cb)
+  prof_ud = newproxy(true)
+  getmetatable(prof_ud).__gc = prof_finish
+end
+
+------------------------------------------------------------------------------
+
+local function start(mode, outfile)
+  if not outfile then outfile = os.getenv("LUAJIT_PROFILEFILE") end
+  if outfile then
+    out = outfile == "-" and stdout or assert(io.open(outfile, "w"))
+  else
+    out = stdout
+  end
+  prof_start(mode or "f")
+end
+
+-- Public module functions.
+return {
+  start = start, -- For -j command line option.
+  stop = prof_finish
+}
+
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/tbl.lua b/lua/plenary/tbl.lua
index d1f018c..44de295 100644
--- a/lua/plenary/tbl.lua
+++ b/lua/plenary/tbl.lua
@@ -24,5 +24,17 @@ function tbl.unpack(t, i, j)
   return unpack(t, i or 1, j or t.n or #t)
 end
 
+---Freeze a table. A frozen table is not able to be modified.
+---http://lua-users.org/wiki/ReadOnlyTables
+---@param t table
+---@return table
+function tbl.freeze(t)
+  return setmetatable({}, {
+    __index = t,
+    __newindex = function()
+      error("Attempt to modify frozen table")
+    end
+  })
+end
 
 return tbl
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..784dec6
--- /dev/null
+++ b/lua/plenary/vararg/rotate.lua
@@ -0,0 +1,71 @@
+---@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(nargs, ...) 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/plugin/plenary.vim b/plugin/plenary.vim
index a6ae381..d0ac14b 100644
--- a/plugin/plenary.vim
+++ b/plugin/plenary.vim
@@ -1,7 +1,4 @@
 
-" Set up neorocks if it is installed.
-lua pcall(function() require('plenary.neorocks').setup_paths() end)
-
 " Create command for running busted
 command! -nargs=1 -complete=file PlenaryBustedFile
       \ lua require('plenary.busted').run(vim.fn.expand("<args>"))
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/neorocks_thoughts.txt b/scratch/neorocks_thoughts.txt
deleted file mode 100644
index b5dfcd1..0000000
--- a/scratch/neorocks_thoughts.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-
-# Happy path
-neorocks.ensure_installed
--> oh hey, neorocks is setup (does nothing)
--> oh hey, already installed
-
-
-# Worst path
-neorocks.install
--> no hererocks.
-    -> wget / curl hererocks
-    -> build / install lua & luarocks
-    -> OK, I'm done
--> once install is done
-    -> build / install the lua package
--> return control flow back?
-
-# First version is, you call install.
-# Opens up window
-# You see that it's doing stuff
-# So you wait (as a person) for it to be done
-# Then you require it.
-
-# Second version is, somehow we do this during startup
-# But still allow you to complete startup w/out freezing for a long time
-
-# Alternative version:
-# Run the install w/ headless nvim
-# Could make a script, available in plenary/scripts or something like that, that allows you to do so.
- nvim --headless -c "lua require('plenary.neorocks').install('lua-cjson')" -c "qa"
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..1e1719c
--- /dev/null
+++ b/scripts/vararg/rotate.lua
@@ -0,0 +1,26 @@
+---@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(nargs, ...)
+  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 1c4bf46..87aeeb4 100644
--- a/lua/plenary/test_harness.lua
+++ b/lua/plenary/test_harness.lua
@@ -67,7 +67,7 @@ function harness.test_directory(directory, opts)
 
   local path_len = #paths
 
-  local jobs = f.map(
+  local jobs = vim.tbl_map(
     function(p)
       local args = {
         '--headless',
@@ -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')
@@ -83,7 +85,7 @@ function harness.test_directory(directory, opts)
       end
 
       return Job:new {
-        command = 'nvim',
+        command = vim.v.progpath,
         args = args,
 
         -- Can be turned on to debug
@@ -145,7 +147,7 @@ function harness._find_files_to_run(directory)
     args = {directory, '-type', 'f', '-name', '*_spec.lua'},
   }
 
-  return f.map(Path.new, finder:sync())
+  return vim.tbl_map(Path.new, finder:sync())
 end
 
 function harness._run_path(test_type, directory)
diff --git a/scratch/here_rock_tester.lua b/scratch/here_rock_tester.lua
deleted file mode 100644
index 21e7c28..0000000
--- a/scratch/here_rock_tester.lua
+++ /dev/null
@@ -1,11 +0,0 @@
-
-require('plenary.reload').reload_module('plenary')
-
--- local rocks = require('nvim_rocks')
--- rocks.ensure_installed('lua-cjson')
--- require('cjson')
-
-local neorocks = require('plenary.neorocks')
-
-neorocks.install('lua-cjson')
--- neorocks.remove('busted')
diff --git a/scratch/test_neorock_stuff.lua b/scratch/test_neorock_stuff.lua
deleted file mode 100644
index 0c71622..0000000
--- a/scratch/test_neorock_stuff.lua
+++ /dev/null
@@ -1,19 +0,0 @@
-require('plenary.reload').reload_module('plenary')
-
--- TODO: We could add some stuff to delete this at the beginning to start fresh.
--- Probably would be useful to have an uninstall anyways
-
-local neorocks = require('plenary.neorocks')
-neorocks.install('luasocket', nil, true)
-
--- local job = neorocks._get_setup_job(false)
--- neorocks.ensure_installed('effil', 'effil')
--- neorocks.ensure_installed('lua-cjson')
--- neorocks.ensure_installed('luasocket')
-
--- if job then
---   job:start()
--- else
---   print("Already installed")
--- end
-
diff --git a/tests/plenary/async/async_spec.lua b/tests/plenary/async/async_spec.lua
new file mode 100644
index 0000000..104083f
--- /dev/null
+++ b/tests/plenary/async/async_spec.lua
@@ -0,0 +1,54 @@
+require('plenary.async').tests.add_to_env()
+
+describe('async', function()
+  a.it('void functions can call wrapped functions', function()
+    local stat = 0
+    local saved_arg
+
+    local wrapped = a.wrap(function(inc, callback)
+      stat = stat + inc
+      callback()
+    end, 2)
+
+    local voided = a.void(function(arg)
+      wrapped(1)
+      wrapped(2)
+      wrapped(3)
+      stat = stat + 1
+      saved_arg = arg
+    end)
+
+    voided('hello')
+
+    assert(stat == 7)
+    assert(saved_arg == 'hello')
+  end)
+
+  a.it('void functions can call wrapped functions with ignored arguments', function()
+    local stat = 0
+    local saved_arg
+
+    local wrapped = a.wrap(function(inc, nil1, nil2, callback)
+      assert(type(inc) == 'number')
+      assert(nil1 == nil)
+      assert(nil2 == nil)
+      assert(type(callback) == 'function')
+      stat = stat + inc
+      callback()
+    end, 4)
+
+    local voided = a.void(function(arg)
+      wrapped(1)
+      wrapped(2, nil)
+      wrapped(3, nil, nil)
+      stat = stat + 1
+      saved_arg = arg
+    end)
+
+    voided('hello')
+
+    assert(stat == 7)
+    assert(saved_arg == 'hello')
+  end)
+
+end)
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 4eb509a..063ae90 100644
--- a/tests/plenary/path_spec.lua
+++ b/tests/plenary/path_spec.lua
@@ -24,6 +24,11 @@ describe('Path', function()
       local p = Path:new { "./home/tj/" , sep = '/'}
       assert(not p:is_absolute(), "Is absolute")
     end)
+
+    it('will normalize the path', function()
+      local p = Path:new { "lua", "..", "README.md" , sep = '/'}
+      assert.are.same(p:absolute(), vim.fn.fnamemodify("README.md", ":p"))
+    end)
   end)
 
   it('can join paths by constructor or join path', function()
@@ -149,20 +154,66 @@ 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()
     it('can take paths with double separators change them to single separators', function()
-      local orig = 'lua//plenary/path.lua'
+      local orig = '/lua//plenary/path.lua'
       local final = Path:new(orig):normalize()
-      assert.are.same(final, 'lua/plenary/path.lua')
+      assert.are.same(final, '/lua/plenary/path.lua')
     end)
     -- this may be redundant since normalize just calls make_relative which is tested above
     it('can take absolute paths with double seps'
       .. 'and make them relative with single seps', function()
-      local orig = vim.loop.cwd() .. '/lua//plenary/path.lua'
+      local orig = '/lua//plenary/path.lua'
+      local final = Path:new(orig):normalize()
+      assert.are.same(final, '/lua/plenary/path.lua')
+    end)
+
+    it('can remove the .. in paths', function()
+      local orig = '/lua//plenary/path.lua/foo/bar/../..'
       local final = Path:new(orig):normalize()
-      assert.are.same(final, 'lua/plenary/path.lua')
+      assert.are.same(final, '/lua/plenary/path.lua')
+    end)
+  end)
+
+  describe(':shorten', function()
+    it('can shorten a path', function()
+      local long_path = '/this/is/a/long/path'
+      local short_path = Path:new(long_path):shorten()
+      assert.are.same(short_path, '/t/i/a/l/path')
+    end)
+  end)
+
+  describe(':shorten', function()
+    it('can shorten a path components to a given length', function()
+      local long_path = '/this/is/a/long/path'
+      local short_path = Path:new(long_path):shorten(2)
+      assert.are.same(short_path, '/th/is/a/lo/path')
+
+      -- without the leading /
+      long_path = 'this/is/a/long/path'
+      short_path = Path:new(long_path):shorten(3)
+      assert.are.same(short_path, 'thi/is/a/lon/path')
+
+      -- where len is greater than the length of the final component
+      long_path = 'this/is/an/extremely/long/path'
+      short_path = Path:new(long_path):shorten(5)
+      assert.are.same(short_path, 'this/is/an/extre/long/path')
     end)
   end)
 
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)
diff --git a/tests/plenary/tbl_spec.lua b/tests/plenary/tbl_spec.lua
new file mode 100644
index 0000000..60faa18
--- /dev/null
+++ b/tests/plenary/tbl_spec.lua
@@ -0,0 +1,27 @@
+local tbl = require('plenary.tbl')
+
+local function should_fail(fun)
+  local stat = pcall(fun)
+  assert(not stat, "Function should have errored")
+end
+
+describe('tbl utilities', function()
+  it('should be able to freeze a table', function()
+    local t = {1, 2, 3}
+    local frozen = tbl.freeze(t)
+    assert(t[1] == frozen[1])
+    assert(t[2] == frozen[2])
+    assert(t[3] == frozen[3])
+
+    should_fail(function()
+      frozen[4] = "thisthis"
+    end)
+
+    should_fail(function()
+      frozen.hello = "asdfasdf"
+    end)
+
+    assert(not frozen[5])
+    assert(not frozen.hello)
+  end)
+end)