Repository: nvim-lua/plenary.nvim
Pull request #195 (2021-07-25 19:51:49)

feat: add option to give a position for `Border` title
Add the ability to define a position for the `title` on the `Border` object.

Picture using `popup.nvim`:
![image](https://user-images.githubusercontent.com/35707277/129421344-78e9d3eb-1cc1-41b1-a3f6-6b5b48ffce16.png)

For `border_win_options.title` there are now two ways of providing a title:
- a string, which then defaults to the position of middle of the top border (`N`) as it previously has
- a list of tables, where the tables have keys `pos` and `text`
  - order of the tables determines priority (only one title each on top and bottom atm)
  - `pos` can have values in `NW`. `N`, `NE`, `SW`, `S`, `SE`
  - `text` is the text to be displayed

Feedback welcome 🙂 

anott03: My one thought is on how you prioritize the left. Given that current usage (something like  `popup.create(..., { ... , title = "title"})`) would expect the title to be centered, I'd suggest prioritizing `top-mid`, at least for some transition period.

Another thought I had (but I think would be hard to implement) is that you prioritize the first key provided. So if I do something along the lines of `popup.create(..., { ... , title = { ['top-right']="title", ['top-left']="title" }})` the title would be placed in the top right as that is the first key provided. Just a thought – I don't see an inherent issue with going from left to right. 

l-kershaw: Hi @anott03, thanks for the feedback 🙂

I agree with your first point, that if no position is provided the title should be in position `top-mid`. This is already implemented [here](https://github.com/nvim-lua/plenary.nvim/blob/b9cf2b0428e50c28be9a4f14297937758c32eed8/lua/plenary/window/border.lua#L72-L73). The priority system only affects things when multiple titles are given for either the top or bottom. For example, currently:
```
popup.create(..., { ... , title = { ["top-left"]="left-title", ["top-right"]="right-title" }})
```
would be equivalent to:
```
popup.create(..., { ... , title = { ["top-left"]="left-title"}})
```
That being said, I am hoping that the priority system is a temporary setup, as I am planning to allow multiple titles on the same line (space allowing) in a future PR.

Regarding your second idea, I don't think that lua keeps the order in which things are added to a table, they are just a collection of key-value pairs. So I don't think that this is possible without refactoring the setup to be a list of tables/lists.
We could change title to being a list, but I'm not really sure what is more user friendly to use/understand:
- current setup, as a table: `title = { ["top-left"]="left-title", ["top-right"]="right-title" }`
- as a list of lists: `title = { {"top-left", "left-title"}, {"top-right", "right-title"} }`

What are your thoughts on table vs. list of lists?


Patch:
diff --git a/.styluaignore b/.styluaignore
index e7bbd37..d3399b4 100644
--- a/.styluaignore
+++ b/.styluaignore
@@ -3,6 +3,7 @@ data/
 lua/luassert
 lua/plenary/profile.lua
 lua/plenary/profile/
+lua/plenary/bit.lua
 lua/say.lua
 scratch/
 scripts/
diff --git a/POPUP.md b/POPUP.md
new file mode 100644
index 0000000..771d581
--- /dev/null
+++ b/POPUP.md
@@ -0,0 +1,86 @@
+# Popup tracking
+
+[WIP] An implementation of the Popup API from vim in Neovim. Hope to upstream
+when complete
+
+## Goals
+
+Provide an API that is compatible with the vim `popup_*` APIs. After
+stablization and any required features are merged into Neovim, we can upstream
+this and expose the API in vimL to create better compatibility.
+
+## List of Neovim Features Required:
+
+- [ ] Add Z-index for floating windows
+    - [ ] When complete, we can add `zindex` parameter
+- [ ] Key handlers (used for `popup_filter`)
+- [ ] scrollbar for floating windows
+    - [ ] scrollbar
+    - [ ] scrollbarhighlight
+    - [ ] thumbhighlight
+
+Optional:
+
+- [ ] Add forced transparency to a floating window.
+    - Apparently overrides text?
+    - This is for the `mask` feature flag
+
+
+Unlikely (due to technical difficulties):
+
+- [ ] Add `textprop` wrappers?
+    - textprop
+    - textpropwin
+    - textpropid
+- [ ] "close"
+    - But this is mostly because I don't know how to use mouse APIs in nvim. If someone knows. please make an issue in the repo, and maybe we can get it sorted out.
+
+Unlikely (due to not sure if people are using):
+- [ ] tabpage
+
+## Progress
+
+Suported Features:
+
+- [x] what
+    - string
+    - list of strings
+- [x] popup_create-arguments
+    - [x] border
+    - [x] borderchars
+    - [x] col
+    - [x] cursorline
+    - [x] highlight
+    - [x] line
+    - [x] {max,min}{height,width}
+    - [?] moved
+        - [x] "any"
+        - [ ] "word"
+        - [ ] "WORD"
+        - [ ] "expr"
+        - [ ] (list options)
+    - [x] padding
+    - [?] pos
+        - Somewhat implemented. Doesn't work with borders though.
+    - [x] posinvert
+    - [x] time
+    - [x] title
+    - [x] wrap
+
+## All known unimplemented vim features at the moment
+
+- firstline
+- hidden
+- ~ pos
+- fixed
+- filter
+- filtermode
+- mapping
+- callback
+- mouse:
+    - mousemoved
+    - close
+    - drag
+    - resize
+
+- (not implemented in vim yet) flip
diff --git a/README.md b/README.md
index 0aa8291..47c9f36 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,8 @@ Plug 'nvim-lua/plenary.nvim'
 
 ## Modules
 
+- `plenary.async`
+- `plenary.async_lib`
 - `plenary.job`
 - `plenary.path`
 - `plenary.scandir`
@@ -26,9 +28,78 @@ Plug 'nvim-lua/plenary.nvim'
 - `plenary.filetype`
 - `plenary.strings`
 
+### plenary.async
+
+A Lua module for asynchronous programming using coroutines. This library is built on native lua coroutines and `libuv`. Coroutines make it easy to avoid callback hell and allow for easy cooperative concurrency and cancellation. Apart from allowing users to perform asynchronous io easily, this library also functions as an abstraction for coroutines.
+
+#### Getting started
+
+You can do
+```lua
+local async = require "plenary.async"
+```
+All other modules are automatically required and can bet accessed by indexing `async`.
+You needn't worry about performance as this will require all the submodules lazily.
+
+#### A quick example
+
+Libuv luv provides this example of reading a file.
+
+```lua
+local uv = vim.loop
+
+local read_file = function(path, callback)
+  uv.fs_open(path, "r", 438, function(err, fd)
+    assert(not err, err)
+    uv.fs_fstat(fd, function(err, stat)
+      assert(not err, err)
+      uv.fs_read(fd, stat.size, 0, function(err, data)
+        assert(not err, err)
+        uv.fs_close(fd, function(err)
+          assert(not err, err)
+          callback(data)
+        end)
+      end)
+    end)
+  end)
+end
+```
+
+We can write it using the library like this:
+```lua
+local a = require "plenary.async"
+
+local read_file = function(path)
+  local err, fd = a.uv.fs_open(path, "r", 438)
+  assert(not err, err)
+
+  local err, stat = a.uv.fs_fstat(fd)
+  assert(not err, err)
+
+  local err, data = a.uv.fs_read(fd, stat.size, 0)
+  assert(not err, err)
+
+  local err = a.uv.fs_close(fd)
+  assert(not err, err)
+
+  return data
+end
+```
+
+#### Plugins using this
+
+- [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim)
+- [gitsigns.nvim](https://github.com/lewis6991/gitsigns.nvim)
+- [vgit.nvim](https://github.com/tanvirtin/vgit.nvim)
+- [neogit](https://github.com/TimUntersberger/neogit) (using version 1)
+
+### plenary.async_lib
+
+Please use `plenary.async` instead. This was version 1 and is just here for compatibility reasons.
+
 ### plenary.job
 
-A Lua module to interactive with system processes. Pass in your `command`, the desired `args`, `env` and `cwd`.
+A Lua module to interact with system processes. Pass in your `command`, the desired `args`, `env` and `cwd`.
 Define optional callbacks for `on_stdout`, `on_stderr` and `on_exit` and `start` your Job.
 
 Note: Each job has an empty environment.
@@ -54,7 +125,7 @@ A Lua module that implements a bunch of the things from `pathlib` from Python, s
 
 ### plenary.scandir
 
-`plenery.scandir` is fast recursive file operations. It is similar to unix `find` or `fd` in that it can do recursive scan over a given directory, or a set of directories.
+`plenery.scandir` is fast recursive file operations. It is similar to unix `find` or `fd` in that it can do recursive scans over a given directory, or a set of directories.
 
 It offers a wide range of opts for limiting the depth, show hidden and more. `plenary.scan_dir` can be ran synchronously and asynchronously and offers `on_insert(file, typ)` and `on_exit(files)` callbacks. `on_insert(file, typ)` is available for both while `on_exit(files)` is only available for async.
 
@@ -100,9 +171,13 @@ nvim --headless -c "PlenaryBustedDirectory tests/plenary/ {minimal_init = 'tests
 ```
 
 Where the first argument is the directory you'd like to test. It will search for files with
-the pattern `*_spec.lua` and execute them in parallel in separate neovim instances.
+the pattern `*_spec.lua` and execute them in separate neovim instances.
 
-The second argument is an optional init.vim to specify so that you can make reproducible tests!
+The second argument is a Lua option table with the following fields:
+- `minimal_init`: specify an init.vim to use for this instance, uses `--noplugin`
+- `minimal`: uses `--noplugin` without an init script (overrides `minimal_init`)
+- `sequential`: whether to run tests sequentially (default is to run in parallel)
+- `keep_going`: if `sequential`, whether to continue on test failure (default true)
 
 The exit code is 0 when success and 1 when fail, so you can use it easily in a `Makefile`!
 
@@ -132,16 +207,8 @@ OTHER OTHER NOTE:
 Take a look at some test examples [here](TESTS_README.md).
 
 #### Colors
-To have "Success" in green and "Failed" in red, you need [nvim-terminal.lua](https://github.com/norcalli/nvim-terminal.lua).
-In order for it to work, make sure to run the setup function in your config.
-For `init.vim`:
-```vim
-lua require('terminal').setup()
-```
-For `init.lua`:
-```lua
-require('terminal').setup()
-```
+
+You no longer need nvim-terminal to get this to work. We use `nvim_open_term` now.
 
 ### plenary.filetype
 
@@ -217,10 +284,7 @@ Status: WIP
 
 ### plenary.popup
 
-`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.
-
-Status: WIP
+See [popup documentation](./POPUP.md) for both progress tracking and implemented APIs.
 
 ### plenary.window
 
diff --git a/lua/plenary/async/api.lua b/lua/plenary/async/api.lua
index ad5eeac..74f1895 100644
--- a/lua/plenary/async/api.lua
+++ b/lua/plenary/async/api.lua
@@ -1,4 +1,3 @@
-local a = require "plenary.async.async"
 local util = require "plenary.async.util"
 
 return setmetatable({}, {
diff --git a/lua/plenary/async/async.lua b/lua/plenary/async/async.lua
index a5f9d38..c3777e8 100644
--- a/lua/plenary/async/async.lua
+++ b/lua/plenary/async/async.lua
@@ -23,10 +23,7 @@ local function callback_or_next(step, thread, callback, ...)
     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
+    returned_function(vararg.rotate(nargs, step, select(4, ...)))
   end
 end
 
diff --git a/lua/plenary/async/control.lua b/lua/plenary/async/control.lua
index 16f963e..257ba71 100644
--- a/lua/plenary/async/control.lua
+++ b/lua/plenary/async/control.lua
@@ -209,7 +209,7 @@ M.channel.mpsc = function()
     end
     local val = deque:popright()
     deque:clear()
-    return unpack(val)
+    return unpack(val or {})
   end
 
   return Sender, Receiver
diff --git a/lua/plenary/async/init.lua b/lua/plenary/async/init.lua
index 83c5aa2..f8222fc 100644
--- a/lua/plenary/async/init.lua
+++ b/lua/plenary/async/init.lua
@@ -3,13 +3,28 @@
 ---         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"
+local lookups = {
+  uv = "plenary.async.uv_async",
+  util = "plenary.async.util",
+  lsp = "plenary.async.lsp",
+  api = "plenary.async.api",
+  tests = "plenary.async.tests",
+  control = "plenary.async.control",
+}
+
+local exports = setmetatable(require "plenary.async.async", {
+  __index = function(t, k)
+    local require_path = lookups[k]
+    if not require_path then
+      return
+    end
+
+    local mod = require(require_path)
+    t[k] = mod
+
+    return mod
+  end,
+})
 
 exports.tests.add_globals = function()
   a = exports
diff --git a/lua/plenary/async/util.lua b/lua/plenary/async/util.lua
index b243a39..ef411e7 100644
--- a/lua/plenary/async/util.lua
+++ b/lua/plenary/async/util.lua
@@ -1,8 +1,5 @@
 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
diff --git a/lua/plenary/bit.lua b/lua/plenary/bit.lua
new file mode 100644
index 0000000..a201d96
--- /dev/null
+++ b/lua/plenary/bit.lua
@@ -0,0 +1,341 @@
+-- Shortcircuit to returning bit if it already exists
+if bit then return bit end
+
+--[[
+
+Credit: https://github.com/davidm/lua-bit-numberlua/blob/master/lmod/bit/numberlua.lua
+
+LUA MODULE
+
+  bit.numberlua - Bitwise operations implemented in pure Lua as numbers,
+    with Lua 5.2 'bit32' and (LuaJIT) LuaBitOp 'bit' compatibility interfaces.
+
+SYNOPSIS
+
+  local bit = require 'bit.numberlua'
+  print(bit.band(0xff00ff00, 0x00ff00ff)) --> 0xffffffff
+
+  -- Interface providing strong (LuaJIT) LuaBitOp 'bit' compatibility
+  local bit = require 'plenary.bit'
+  assert(bit.tobit(0xffffffff) == -1)
+
+  REMOVED!
+  -- Interface providing strong Lua 5.2 'bit32' compatibility
+  local bit32 = require 'bit.numberlua'.bit32
+  assert(bit32.band(-1) == 0xffffffff)
+
+
+DESCRIPTION
+
+  This library implements bitwise operations entirely in Lua.
+  This module is typically intended if for some reasons you don't want
+  to or cannot  install a popular C based bit library like BitOp 'bit' [1]
+  (which comes pre-installed with LuaJIT) or 'bit32' (which comes
+  pre-installed with Lua 5.2) but want a similar interface.
+
+  This modules represents bit arrays as non-negative Lua numbers. [1]
+  It can represent 32-bit bit arrays when Lua is compiled
+  with lua_Number as double-precision IEEE 754 floating point.
+
+  The module is nearly the most efficient it can be but may be a few times
+  slower than the C based bit libraries and is orders or magnitude
+  slower than LuaJIT bit operations, which compile to native code.  Therefore,
+  this library is inferior in performane to the other modules.
+
+  The `xor` function in this module is based partly on Roberto Ierusalimschy's
+  post in http://lua-users.org/lists/lua-l/2002-09/msg00134.html .
+
+  The included BIT.bit32 and BIT.bit sublibraries aims to provide 100%
+  compatibility with the Lua 5.2 "bit32" and (LuaJIT) LuaBitOp "bit" library.
+  This compatbility is at the cost of some efficiency since inputted
+  numbers are normalized and more general forms (e.g. multi-argument
+  bitwise operators) are supported.
+
+STATUS
+
+  WARNING: Not all corner cases have been tested and documented.
+  Some attempt was made to make these similar to the Lua 5.2 [2]
+  and LuaJit BitOp [3] libraries, but this is not fully tested and there
+  are currently some differences.  Addressing these differences may
+  be improved in the future but it is not yet fully determined how to
+  resolve these differences.
+
+  The BIT.bit32 library passes the Lua 5.2 test suite (bitwise.lua)
+  http://www.lua.org/tests/5.2/ .  The BIT.bit library passes the LuaBitOp
+  test suite (bittest.lua).  However, these have not been tested on
+  platforms with Lua compiled with 32-bit integer numbers.
+
+API
+
+  Module's return
+
+    This table contains functions that aim to provide 100% compatibility
+    with the LuaBitOp "bit" library (from LuaJIT).
+
+    bit.tobit(x) --> y
+    bit.tohex(x [,n]) --> y
+    bit.bnot(x) --> y
+    bit.bor(x1 [,x2...]) --> y
+    bit.band(x1 [,x2...]) --> y
+    bit.bxor(x1 [,x2...]) --> y
+    bit.lshift(x, n) --> y
+    bit.rshift(x, n) --> y
+    bit.arshift(x, n) --> y
+    bit.rol(x, n) --> y
+    bit.ror(x, n) --> y
+    bit.bswap(x) --> y
+
+DEPENDENCIES
+
+  None (other than Lua 5.1 or 5.2).
+
+REFERENCES
+
+  [1] http://lua-users.org/wiki/FloatingPoint
+  [2] http://www.lua.org/manual/5.2/
+  [3] http://bitop.luajit.org/
+
+LICENSE
+
+  (c) 2008-2011 David Manura.  Licensed under the same terms as Lua (MIT).
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+  (end license)
+
+  Some modifications by plenary team.
+
+--]]
+
+local M = {_TYPE='module', _NAME='bit.numberlua', _VERSION='0.3.1.20120131'}
+
+local floor = math.floor
+
+local MOD = 2^32
+local MODM = MOD-1
+
+local function memoize(f)
+  local mt = {}
+  local t = setmetatable({}, mt)
+  function mt:__index(k)
+    local v = f(k); t[k] = v
+    return v
+  end
+  return t
+end
+
+local function make_bitop_uncached(t, m)
+  local function bitop(a, b)
+    local res,p = 0,1
+    while a ~= 0 and b ~= 0 do
+      local am, bm = a%m, b%m
+      res = res + t[am][bm]*p
+      a = (a - am) / m
+      b = (b - bm) / m
+      p = p*m
+    end
+    res = res + (a+b)*p
+    return res
+  end
+  return bitop
+end
+
+local function make_bitop(t)
+  local op1 = make_bitop_uncached(t,2^1)
+  local op2 = memoize(function(a)
+    return memoize(function(b)
+      return op1(a, b)
+    end)
+  end)
+  return make_bitop_uncached(op2, 2^(t.n or 1))
+end
+
+-- ok?  probably not if running on a 32-bit int Lua number type platform
+function M.tobit(x)
+  return x % 2^32
+end
+
+M.bxor = make_bitop {[0]={[0]=0,[1]=1},[1]={[0]=1,[1]=0}, n=4}
+local bxor = M.bxor
+
+function M.bnot(a)   return MODM - a end
+local bnot = M.bnot
+
+function M.band(a,b) return ((a+b) - bxor(a,b))/2 end
+local band = M.band
+
+function M.bor(a,b)  return MODM - band(MODM - a, MODM - b) end
+local bor = M.bor
+
+local lshift, rshift -- forward declare
+
+function M.rshift(a,disp) -- Lua5.2 insipred
+  if disp < 0 then return lshift(a,-disp) end
+  return floor(a % 2^32 / 2^disp)
+end
+rshift = M.rshift
+
+function M.lshift(a,disp) -- Lua5.2 inspired
+  if disp < 0 then return rshift(a,-disp) end
+  return (a * 2^disp) % 2^32
+end
+lshift = M.lshift
+
+function M.tohex(x, n) -- BitOp style
+  n = n or 8
+  local up
+  if n <= 0 then
+    if n == 0 then return '' end
+    up = true
+    n = - n
+  end
+  x = band(x, 16^n-1)
+  return ('%0'..n..(up and 'X' or 'x')):format(x)
+end
+local tohex = M.tohex
+
+function M.extract(n, field, width) -- Lua5.2 inspired
+  width = width or 1
+  return band(rshift(n, field), 2^width-1)
+end
+local extract = M.extract
+
+function M.replace(n, v, field, width) -- Lua5.2 inspired
+  width = width or 1
+  local mask1 = 2^width-1
+  v = band(v, mask1) -- required by spec?
+  local mask = bnot(lshift(mask1, field))
+  return band(n, mask) + lshift(v, field)
+end
+local replace = M.replace
+
+function M.bswap(x)  -- BitOp style
+  local a = band(x, 0xff); x = rshift(x, 8)
+  local b = band(x, 0xff); x = rshift(x, 8)
+  local c = band(x, 0xff); x = rshift(x, 8)
+  local d = band(x, 0xff)
+  return lshift(lshift(lshift(a, 8) + b, 8) + c, 8) + d
+end
+local bswap = M.bswap
+
+function M.rrotate(x, disp)  -- Lua5.2 inspired
+  disp = disp % 32
+  local low = band(x, 2^disp-1)
+  return rshift(x, disp) + lshift(low, 32-disp)
+end
+local rrotate = M.rrotate
+
+function M.lrotate(x, disp)  -- Lua5.2 inspired
+  return rrotate(x, -disp)
+end
+local lrotate = M.lrotate
+
+M.rol = M.lrotate  -- LuaOp inspired
+M.ror = M.rrotate  -- LuaOp insipred
+
+
+function M.arshift(x, disp) -- Lua5.2 inspired
+  local z = rshift(x, disp)
+  if x >= 0x80000000 then z = z + lshift(2^disp-1, 32-disp) end
+  return z
+end
+local arshift = M.arshift
+
+function M.btest(x, y) -- Lua5.2 inspired
+  return band(x, y) ~= 0
+end
+
+--
+-- Start LuaBitOp "bit" compat section.
+--
+
+M.bit = {} -- LuaBitOp "bit" compatibility
+
+function M.bit.tobit(x)
+  x = x % MOD
+  if x >= 0x80000000 then x = x - MOD end
+  return x
+end
+local bit_tobit = M.bit.tobit
+
+function M.bit.tohex(x, ...)
+  return tohex(x % MOD, ...)
+end
+
+function M.bit.bnot(x)
+  return bit_tobit(bnot(x % MOD))
+end
+
+local function bit_bor(a, b, c, ...)
+  if c then
+    return bit_bor(bit_bor(a, b), c, ...)
+  elseif b then
+    return bit_tobit(bor(a % MOD, b % MOD))
+  else
+    return bit_tobit(a)
+  end
+end
+M.bit.bor = bit_bor
+
+local function bit_band(a, b, c, ...)
+  if c then
+    return bit_band(bit_band(a, b), c, ...)
+  elseif b then
+    return bit_tobit(band(a % MOD, b % MOD))
+  else
+    return bit_tobit(a)
+  end
+end
+M.bit.band = bit_band
+
+local function bit_bxor(a, b, c, ...)
+  if c then
+    return bit_bxor(bit_bxor(a, b), c, ...)
+  elseif b then
+    return bit_tobit(bxor(a % MOD, b % MOD))
+  else
+    return bit_tobit(a)
+  end
+end
+M.bit.bxor = bit_bxor
+
+function M.bit.lshift(x, n)
+  return bit_tobit(lshift(x % MOD, n % 32))
+end
+
+function M.bit.rshift(x, n)
+  return bit_tobit(rshift(x % MOD, n % 32))
+end
+
+function M.bit.arshift(x, n)
+  return bit_tobit(arshift(x % MOD, n % 32))
+end
+
+function M.bit.rol(x, n)
+  return bit_tobit(lrotate(x % MOD, n % 32))
+end
+
+function M.bit.ror(x, n)
+  return bit_tobit(rrotate(x % MOD, n % 32))
+end
+
+function M.bit.bswap(x)
+  return bit_tobit(bswap(x % MOD))
+end
+
+return M.bit
diff --git a/lua/plenary/curl.lua b/lua/plenary/curl.lua
index c234833..b280b1f 100644
--- a/lua/plenary/curl.lua
+++ b/lua/plenary/curl.lua
@@ -65,11 +65,16 @@ util.kv_to_str = function(kv, sep, kvsep)
 end
 
 util.gen_dump_path = function()
+  local path
   local id = string.gsub("xxxx4xxx", "[xy]", function(l)
     local v = (l == "x") and math.random(0, 0xf) or math.random(0, 0xb)
     return string.format("%x", v)
   end)
-  local path = "/tmp/plenary_curl_" .. id .. ".headers"
+  if P.path.sep == "\\" then
+    path = string.format("%s\\AppData\\Local\\Temp\\plenary_curl_%s.headers", os.getenv "USERPROFILE", id)
+  else
+    path = "/tmp/plenary_curl_" .. id .. ".headers"
+  end
   return { "-D", path }
 end
 
diff --git a/lua/plenary/json.lua b/lua/plenary/json.lua
new file mode 100644
index 0000000..bbcd7d5
--- /dev/null
+++ b/lua/plenary/json.lua
@@ -0,0 +1,102 @@
+-- based on https://github.com/sindresorhus/strip-json-comments
+
+local singleComment = "singleComment"
+local multiComment = "multiComment"
+local stripWithoutWhitespace = function()
+  return ""
+end
+
+local function slice(str, from, to)
+  from = from or 1
+  to = to or #str
+  return str:sub(from, to)
+end
+
+local stripWithWhitespace = function(str, from, to)
+  return slice(str, from, to):gsub("%S", " ")
+end
+
+local isEscaped = function(jsonString, quotePosition)
+  local index = quotePosition - 1
+  local backslashCount = 0
+
+  while jsonString:sub(index, index) == "\\" do
+    index = index - 1
+    backslashCount = backslashCount + 1
+  end
+  return backslashCount % 2 == 1 and true or false
+end
+
+local M = {}
+
+-- Strips any json comments from a json string.
+-- The resulting string can then be used by `vim.fn.json_decode`
+--
+---@param jsonString string
+---@param options table
+---  * whitespace:
+---     - defaults to true
+---     - when true, comments will be replaced by whitespace
+---     - when false, comments will be stripped
+function M.json_strip_comments(jsonString, options)
+  options = options or {}
+  local strip = options.whitespace == false and stripWithoutWhitespace or stripWithWhitespace
+
+  local insideString = false
+  local insideComment = false
+  local offset = 1
+  local result = ""
+  local skip = false
+
+  for i = 1, #jsonString, 1 do
+    if skip then
+      skip = false
+    else
+      local currentCharacter = jsonString:sub(i, i)
+      local nextCharacter = jsonString:sub(i + 1, i + 1)
+
+      if not insideComment and currentCharacter == '"' then
+        local escaped = isEscaped(jsonString, i)
+        if not escaped then
+          insideString = not insideString
+        end
+      end
+
+      if not insideString then
+        if not insideComment and currentCharacter .. nextCharacter == "//" then
+          result = result .. slice(jsonString, offset, i - 1)
+          offset = i
+          insideComment = singleComment
+          i = i + 1
+          skip = true
+        elseif insideComment == singleComment and currentCharacter .. nextCharacter == "\r\n" then
+          i = i + 1
+          skip = true
+          insideComment = false
+          result = result .. strip(jsonString, offset, i - 1)
+          offset = i
+        elseif insideComment == singleComment and currentCharacter == "\n" then
+          insideComment = false
+          result = result .. strip(jsonString, offset, i - 1)
+          offset = i
+        elseif not insideComment and currentCharacter .. nextCharacter == "/*" then
+          result = result .. slice(jsonString, offset, i - 1)
+          offset = i
+          insideComment = multiComment
+          i = i + 1
+          skip = true
+        elseif insideComment == multiComment and currentCharacter .. nextCharacter == "*/" then
+          i = i + 1
+          skip = true
+          insideComment = false
+          result = result .. strip(jsonString, offset, i)
+          offset = i + 1
+        end
+      end
+    end
+  end
+
+  return result .. (insideComment and strip(slice(jsonString, offset)) or slice(jsonString, offset))
+end
+
+return M
diff --git a/lua/plenary/log.lua b/lua/plenary/log.lua
index c4d6dfc..117dcd2 100644
--- a/lua/plenary/log.lua
+++ b/lua/plenary/log.lua
@@ -111,7 +111,7 @@ log.new = function(config, standalone)
 
         local split_console = vim.split(console_string, "\n")
         for _, v in ipairs(split_console) do
-          local formatted_msg = string.format("[%s] %s", config.plugin, vim.fn.escape(v, '"'))
+          local formatted_msg = string.format("[%s] %s", config.plugin, vim.fn.escape(v, [["\]]))
 
           local ok = pcall(vim.cmd, string.format([[echom "%s"]], formatted_msg))
           if not ok then
diff --git a/lua/plenary/path.lua b/lua/plenary/path.lua
index edddbe9..9eb0b17 100644
--- a/lua/plenary/path.lua
+++ b/lua/plenary/path.lua
@@ -3,6 +3,7 @@
 --- Goal: Create objects that are extremely similar to Python's `Path` Objects.
 --- Reference: https://docs.python.org/3/library/pathlib.html
 
+local bit = require "plenary.bit"
 local uv = vim.loop
 
 local F = require "plenary.functional"
@@ -71,7 +72,15 @@ local _split_by_separator = (function()
   end
 end)()
 
+local is_uri = function(filename)
+  return string.match(filename, "^%w+://") ~= nil
+end
+
 local function _normalize_path(filename)
+  if is_uri(filename) then
+    return filename
+  end
+
   local out_file = filename
 
   local has = string.find(filename, "..", 1, true)
@@ -96,6 +105,10 @@ local function _normalize_path(filename)
 end
 
 local clean = function(pathname)
+  if is_uri(pathname) then
+    return pathname
+  end
+
   -- Remove double path seps, it's annoying
   pathname = pathname:gsub(path.sep .. path.sep, path.sep)
 
@@ -142,7 +155,6 @@ end
 
 -- TODO: See where we concat the table, and maybe we could make this work.
 Path.__concat = function(self, other)
-  print(self, other)
   return self.filename .. other
 end
 
@@ -250,6 +262,10 @@ function Path:exists()
 end
 
 function Path:expand()
+  if is_uri(self.filename) then
+    return self.filename
+  end
+
   -- TODO support windows
   local expanded
   if string.find(self.filename, "~") then
@@ -274,6 +290,10 @@ function Path:expand()
 end
 
 function Path:make_relative(cwd)
+  if is_uri(self.filename) then
+    return self.filename
+  end
+
   self.filename = clean(self.filename)
   cwd = clean(F.if_nil(cwd, self._cwd, cwd))
   if self.filename == cwd then
@@ -292,6 +312,10 @@ function Path:make_relative(cwd)
 end
 
 function Path:normalize(cwd)
+  if is_uri(self.filename) then
+    return self.filename
+  end
+
   self:make_relative(cwd)
 
   -- Substitute home directory w/ "~"
@@ -329,7 +353,7 @@ local shorten = (function()
     char_u *shorten_dir(char_u *str);
     ]]
     return function(filename)
-      if not filename then
+      if not filename or is_uri(filename) then
         return filename
       end
 
diff --git a/lua/plenary/popup.lua b/lua/plenary/popup.lua
deleted file mode 100644
index bb53c7d..0000000
--- a/lua/plenary/popup.lua
+++ /dev/null
@@ -1,209 +0,0 @@
---- popup.lua
----
---- Wrapper to make the popup api from vim in neovim.
---- Hope to get this part merged in at some point in the future.
-
-local vim = vim
-
-local Border = require "plenary.window.border"
-
-local popup = {}
-
-popup._pos_map = {
-  topleft = "NW",
-  topright = "NE",
-  botleft = "SW",
-  botright = "SE",
-}
-
--- Keep track of hidden popups, so we can load them with popup.show()
-popup._hidden = {}
-
-local function dict_default(options, key, default)
-  if options[key] == nil then
-    return default[key]
-  else
-    return options[key]
-  end
-end
-
-function popup.popup_create(what, vim_options)
-  local bufnr
-  if type(what) == "number" then
-    bufnr = what
-  else
-    bufnr = vim.api.nvim_create_buf(false, true)
-    assert(bufnr, "Failed to create buffer")
-
-    -- TODO: Handle list of lines
-    vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { what })
-  end
-
-  local option_defaults = {
-    posinvert = true,
-  }
-
-  local win_opts = {}
-
-  if vim_options.line then
-    -- TODO: Need to handle "cursor", "cursor+1", ...
-    win_opts.row = vim_options.line
-  else
-    -- TODO: It says it needs to be "vertically cenetered"?...
-    -- wut is that.
-    win_opts.row = 0
-  end
-
-  if vim_options.col then
-    -- TODO: Need to handle "cursor", "cursor+1", ...
-    win_opts.col = vim_options.col
-  else
-    -- TODO: It says it needs to be "horizontally cenetered"?...
-    win_opts.col = 0
-  end
-
-  if vim_options.pos then
-    if vim_options.pos == "center" then
-      -- TODO: Do centering..
-    else
-      win_opts.anchor = popup._pos_map[vim_options.pos]
-    end
-  end
-
-  -- posinvert	When FALSE the value of "pos" is always used.  When
-  -- 		TRUE (the default) and the popup does not fit
-  -- 		vertically and there is more space on the other side
-  -- 		then the popup is placed on the other side of the
-  -- 		position indicated by "line".
-  if dict_default(vim_options, "posinvert", option_defaults) then
-    -- TODO: handle the invert thing
-  end
-
-  -- 	fixed		When FALSE (the default), and:
-  -- 			 - "pos" is "botleft" or "topleft", and
-  -- 			 - "wrap" is off, and
-  -- 			 - the popup would be truncated at the right edge of
-  -- 			   the screen, then
-  -- 			the popup is moved to the left so as to fit the
-  -- 			contents on the screen.  Set to TRUE to disable this.
-
-  win_opts.style = "minimal"
-
-  -- Feels like maxheigh, minheight, maxwidth, minwidth will all be related
-  win_opts.height = 5
-  win_opts.width = 25
-
-  -- textprop	When present the popup is positioned next to a text
-  -- 		property with this name and will move when the text
-  -- 		property moves.  Use an empty string to remove.  See
-  -- 		|popup-textprop-pos|.
-  -- related:
-  --   textpropwin
-  --   textpropid
-
-  -- border
-  local border_options = {}
-  if vim_options.border then
-    local b_top, b_rgight, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft
-    if vim_options.borderchars == nil then
-      b_top, b_rgight, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = {
-        "-",
-        "|",
-        "-",
-        "|",
-        "┌",
-        "┐",
-        "┘",
-        "└",
-      }
-    elseif #vim_options.borderchars == 1 then
-      -- TODO: Unpack 8 times cool to the same vars
-      print "..."
-    elseif #vim_options.borderchars == 2 then
-      -- TODO: Unpack to edges & corners
-      print "..."
-    elseif #vim_options.borderchars == 8 then
-      b_top, b_rgight, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = vim_options.borderhighlight
-    end
-  end
-
-  win_opts.relative = "editor"
-
-  local win_id
-  if vim_options.hidden then
-    assert(false, "I have not implemented this yet and don't know how")
-  else
-    win_id = vim.api.nvim_open_win(bufnr, true, win_opts)
-  end
-
-  -- Moved, handled after since we need the window ID
-  if vim_options.moved then
-    if vim_options.moved == "any" then
-      vim.lsp.util.close_preview_autocmd({ "CursorMoved", "CursorMovedI" }, win_id)
-    elseif vim_options.moved == "word" then
-      -- TODO: Handle word, WORD, expr, and the range functions... which seem hard?
-    end
-  else
-    vim.cmd(string.format("autocmd BufLeave <buffer=%s> ++once call nvim_win_close(%s, v:false)", bufnr, win_id))
-  end
-
-  if vim_options.time then
-    local timer = vim.loop.new_timer()
-    timer:start(
-      vim_options.time,
-      0,
-      vim.schedule_wrap(function()
-        vim.api.nvim_close_win(win_id, false)
-      end)
-    )
-  end
-
-  -- Buffer Options
-  if vim_options.cursorline then
-    vim.api.nvim_win_set_option(0, "cursorline", true)
-  end
-
-  -- vim.api.nvim_win_set_option(0, 'wrap', dict_default(vim_options, 'wrap', option_defaults))
-
-  -- ===== Not Implemented Options =====
-  -- flip: not implemented at the time of writing
-  -- Mouse:
-  --    mousemoved: no idea how to do the things with the mouse, so it's an exercise for the reader.
-  --    drag: mouses are hard
-  --    resize: mouses are hard
-  --    close: mouses are hard
-  --
-  -- scrollbar
-  -- scrollbarhighlight
-  -- thumbhighlight
-  --
-  -- tabpage: seems useless
-
-  -- Create border
-
-  -- title
-  if vim_options.title then
-    border_options.title = vim_options.title
-
-    if vim_options.border == 0 or vim_options.border == nil then
-      vim_options.border = 1
-      border_options.width = 1
-    end
-  end
-
-  if vim_options.border then
-    Border:new(bufnr, win_id, win_opts, border_options)
-  end
-
-  -- TODO: Perhaps there's a way to return an object that looks like a window id,
-  --    but actually has some extra metadata about it.
-  --
-  --    This would make `hidden` a lot easier to manage
-  return win_id
-end
-
-function popup.show(self, asdf) end
-
-popup.show = function() end
-
-return popup
diff --git a/lua/plenary/popup/init.lua b/lua/plenary/popup/init.lua
new file mode 100644
index 0000000..c94ceea
--- /dev/null
+++ b/lua/plenary/popup/init.lua
@@ -0,0 +1,400 @@
+--- popup.lua
+---
+--- Wrapper to make the popup api from vim in neovim.
+--- Hope to get this part merged in at some point in the future.
+---
+--- Please make sure to update "POPUP.md" with any changes and/or notes.
+
+local Border = require "plenary.window.border"
+local Window = require "plenary.window"
+local utils = require "plenary.popup.utils"
+
+local popup = {}
+
+popup._pos_map = {
+  topleft = "NW",
+  topright = "NE",
+  botleft = "SW",
+  botright = "SE",
+}
+
+-- Keep track of hidden popups, so we can load them with popup.show()
+popup._hidden = {}
+
+local function dict_default(options, key, default)
+  if options[key] == nil then
+    return default[key]
+  else
+    return options[key]
+  end
+end
+
+-- Callbacks to be called later by popup.execute_callback
+popup._callbacks = {}
+
+function popup.create(what, vim_options)
+  local bufnr
+  if type(what) == "number" then
+    bufnr = what
+  else
+    bufnr = vim.api.nvim_create_buf(false, true)
+    assert(bufnr, "Failed to create buffer")
+
+    vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
+
+    -- TODO: Handle list of lines
+    if type(what) == "string" then
+      what = { what }
+    else
+      assert(type(what) == "table", '"what" must be a table')
+    end
+
+    -- padding    List with numbers, defining the padding
+    --     above/right/below/left of the popup (similar to CSS).
+    --     An empty list uses a padding of 1 all around.  The
+    --     padding goes around the text, inside any border.
+    --     Padding uses the 'wincolor' highlight.
+    --     Example: [1, 2, 1, 3] has 1 line of padding above, 2
+    --     columns on the right, 1 line below and 3 columns on
+    --     the left.
+    if vim_options.padding then
+      local pad_top, pad_right, pad_below, pad_left
+      if vim.tbl_isempty(vim_options.padding) then
+        pad_top = 1
+        pad_right = 1
+        pad_below = 1
+        pad_left = 1
+      else
+        local padding = vim_options.padding
+        pad_top = padding[1] or 0
+        pad_right = padding[2] or 0
+        pad_below = padding[3] or 0
+        pad_left = padding[4] or 0
+      end
+
+      local left_padding = string.rep(" ", pad_left)
+      local right_padding = string.rep(" ", pad_right)
+      for index = 1, #what do
+        what[index] = string.format("%s%s%s", left_padding, what[index], right_padding)
+      end
+
+      for _ = 1, pad_top do
+        table.insert(what, 1, "")
+      end
+
+      for _ = 1, pad_below do
+        table.insert(what, "")
+      end
+    end
+
+    vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, what)
+  end
+
+  local option_defaults = {
+    posinvert = true,
+  }
+
+  local win_opts = {}
+  win_opts.relative = "editor"
+
+  local cursor_relative_pos = function(pos_str, dim)
+    assert(string.find(pos_str, "^cursor"), "Invalid value for " .. dim)
+    win_opts.relative = "cursor"
+    local line = 0
+    if (pos_str):match "cursor%+(%d+)" then
+      line = line + tonumber((pos_str):match "cursor%+(%d+)")
+    elseif (pos_str):match "cursor%-(%d+)" then
+      line = line - tonumber((pos_str):match "cursor%-(%d+)")
+    end
+    return line
+  end
+
+  if vim_options.line then
+    if type(vim_options.line) == "string" then
+      win_opts.row = cursor_relative_pos(vim_options.line, "row")
+    else
+      win_opts.row = vim_options.line
+    end
+  else
+    -- TODO: It says it needs to be "vertically cenetered"?...
+    -- wut is that.
+    win_opts.row = 0
+  end
+
+  if vim_options.col then
+    if type(vim_options.col) == "string" then
+      win_opts.col = cursor_relative_pos(vim_options.col, "col")
+    else
+      win_opts.col = vim_options.col
+    end
+  else
+    -- TODO: It says it needs to be "horizontally cenetered"?...
+    win_opts.col = 0
+  end
+
+  if vim_options.pos then
+    if vim_options.pos == "center" then
+      -- TODO: Do centering..
+    else
+      win_opts.anchor = popup._pos_map[vim_options.pos]
+    end
+  else
+    win_opts.anchor = "NW" -- This is the default, but makes `posinvert` easier to implement
+  end
+
+  -- , fixed    When FALSE (the default), and:
+  -- ,      - "pos" is "botleft" or "topleft", and
+  -- ,      - "wrap" is off, and
+  -- ,      - the popup would be truncated at the right edge of
+  -- ,        the screen, then
+  -- ,     the popup is moved to the left so as to fit the
+  -- ,     contents on the screen.  Set to TRUE to disable this.
+
+  win_opts.style = "minimal"
+
+  -- Feels like maxheight, minheight, maxwidth, minwidth will all be related
+  --
+  -- maxheight  Maximum height of the contents, excluding border and padding.
+  -- minheight  Minimum height of the contents, excluding border and padding.
+  -- maxwidth  Maximum width of the contents, excluding border, padding and scrollbar.
+  -- minwidth  Minimum width of the contents, excluding border, padding and scrollbar.
+  local width = vim_options.width or 1
+  local height
+  if type(what) == "number" then
+    height = vim.api.nvim_buf_line_count(what)
+  else
+    for _, v in ipairs(what) do
+      width = math.max(width, #v)
+    end
+    height = #what
+  end
+  win_opts.width = utils.bounded(width, vim_options.minwidth, vim_options.maxwidth)
+  win_opts.height = utils.bounded(height, vim_options.minheight, vim_options.maxheight)
+
+  -- posinvert, When FALSE the value of "pos" is always used.  When
+  -- ,   TRUE (the default) and the popup does not fit
+  -- ,   vertically and there is more space on the other side
+  -- ,   then the popup is placed on the other side of the
+  -- ,   position indicated by "line".
+  if dict_default(vim_options, "posinvert", option_defaults) then
+    if win_opts.anchor == "NW" or win_opts.anchor == "NE" then
+      if win_opts.row + win_opts.height > vim.o.lines and win_opts.row * 2 > vim.o.lines then
+        -- Don't know why, but this is how vim adjusts it
+        win_opts.row = win_opts.row - win_opts.height - 2
+      end
+    elseif win_opts.anchor == "SW" or win_opts.anchor == "SE" then
+      if win_opts.row - win_opts.height < 0 and win_opts.row * 2 < vim.o.lines then
+        -- Don't know why, but this is how vim adjusts it
+        win_opts.row = win_opts.row + win_opts.height + 2
+      end
+    end
+  end
+
+  -- textprop, When present the popup is positioned next to a text
+  -- ,   property with this name and will move when the text
+  -- ,   property moves.  Use an empty string to remove.  See
+  -- ,   |popup-textprop-pos|.
+  -- related:
+  --   textpropwin
+  --   textpropid
+
+  local win_id
+  if vim_options.hidden then
+    assert(false, "I have not implemented this yet and don't know how")
+  else
+    win_id = vim.api.nvim_open_win(bufnr, false, win_opts)
+  end
+
+  -- Moved, handled after since we need the window ID
+  if vim_options.moved then
+    if vim_options.moved == "any" then
+      vim.lsp.util.close_preview_autocmd({ "CursorMoved", "CursorMovedI" }, win_id)
+    elseif vim_options.moved == "word" then
+      -- TODO: Handle word, WORD, expr, and the range functions... which seem hard?
+    end
+  else
+    local silent = false
+    vim.cmd(
+      string.format(
+        "autocmd BufDelete %s <buffer=%s> ++once ++nested :lua require('plenary.window').try_close(%s, true)",
+        (silent and "<silent>") or "",
+        bufnr,
+        win_id
+      )
+    )
+  end
+
+  if vim_options.time then
+    local timer = vim.loop.new_timer()
+    timer:start(
+      vim_options.time,
+      0,
+      vim.schedule_wrap(function()
+        Window.try_close(win_id, false)
+      end)
+    )
+  end
+
+  -- Buffer Options
+  if vim_options.cursorline then
+    vim.api.nvim_win_set_option(win_id, "cursorline", true)
+  end
+
+  if vim_options.wrap ~= nil then
+    vim.api.nvim_win_set_option(win_id, "wrap", vim_options.wrap)
+  end
+
+  -- ===== Not Implemented Options =====
+  -- flip: not implemented at the time of writing
+  -- Mouse:
+  --    mousemoved: no idea how to do the things with the mouse, so it's an exercise for the reader.
+  --    drag: mouses are hard
+  --    resize: mouses are hard
+  --    close: mouses are hard
+  --
+  -- scrollbar
+  -- scrollbarhighlight
+  -- thumbhighlight
+
+  -- tabpage: seems useless
+
+  -- Create border
+
+  local should_show_border = nil
+  local border_options = {}
+
+  -- border    List with numbers, defining the border thickness
+  --     above/right/below/left of the popup (similar to CSS).
+  --     Only values of zero and non-zero are recognized.
+  --     An empty list uses a border all around.
+  if vim_options.border then
+    should_show_border = true
+
+    if type(vim_options.border) == "boolean" or vim.tbl_isempty(vim_options.border) then
+      border_options.border_thickness = Border._default_thickness
+    elseif #vim_options.border == 4 then
+      border_options.border_thickness = {
+        top = utils.bounded(vim_options.border[1], 0, 1),
+        right = utils.bounded(vim_options.border[2], 0, 1),
+        bot = utils.bounded(vim_options.border[3], 0, 1),
+        left = utils.bounded(vim_options.border[4], 0, 1),
+      }
+    else
+      error(string.format("Invalid configuration for border: %s", vim.inspect(vim_options.border)))
+    end
+  elseif vim_options.border == false then
+    should_show_border = false
+  end
+
+  if (should_show_border == nil or should_show_border) and vim_options.borderchars then
+    should_show_border = true
+
+    -- borderchars  List with characters, defining the character to use
+    --     for the top/right/bottom/left border.  Optionally
+    --     followed by the character to use for the
+    --     topleft/topright/botright/botleft corner.
+    --     Example: ['-', '|', '-', '|', '┌', '┐', '┘', '└']
+    --     When the list has one character it is used for all.
+    --     When the list has two characters the first is used for
+    --     the border lines, the second for the corners.
+    --     By default a double line is used all around when
+    --     'encoding' is "utf-8" and 'ambiwidth' is "single",
+    --     otherwise ASCII characters are used.
+
+    local b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft
+    if vim_options.borderchars == nil then
+      b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft =
+        "═", "║", "═", "║", "╔", "╗", "╝", "╚"
+    elseif #vim_options.borderchars == 1 then
+      local b_char = vim_options.borderchars[1]
+      b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft =
+        b_char, b_char, b_char, b_char, b_char, b_char, b_char, b_char
+    elseif #vim_options.borderchars == 2 then
+      local b_char = vim_options.borderchars[1]
+      local c_char = vim_options.borderchars[2]
+      b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft =
+        b_char, b_char, b_char, b_char, c_char, c_char, c_char, c_char
+    elseif #vim_options.borderchars == 8 then
+      b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = unpack(vim_options.borderchars)
+    else
+      error(string.format 'Not enough arguments for "borderchars"')
+    end
+
+    border_options.top = b_top
+    border_options.bot = b_bot
+    border_options.right = b_right
+    border_options.left = b_left
+    border_options.topleft = b_topleft
+    border_options.topright = b_topright
+    border_options.botright = b_botright
+    border_options.botleft = b_botleft
+  end
+
+  -- title
+  if vim_options.title then
+    -- TODO: Check out how title works with weird border combos.
+    border_options.title = vim_options.title
+  end
+
+  local border = nil
+  if should_show_border then
+    border = Border:new(bufnr, win_id, win_opts, border_options)
+  end
+
+  if vim_options.highlight then
+    vim.api.nvim_win_set_option(win_id, "winhl", string.format("Normal:%s", vim_options.highlight))
+  end
+
+  if vim_options.borderhighlight then
+    vim.api.nvim_win_set_option(border.win_id, "winhl", string.format("Normal:%s", vim_options.borderhighlight))
+  end
+
+  -- enter
+  local should_enter = vim_options.enter
+  if should_enter == nil then
+    should_enter = true
+  end
+
+  if should_enter then
+    -- set focus after border creation so that it's properly placed (especially
+    -- in relative cursor layout)
+    vim.api.nvim_set_current_win(win_id)
+  end
+
+  -- callback
+  if vim_options.callback then
+    popup._callbacks[bufnr] = function()
+      -- (jbyuki): Giving win_id is pointless here because it's closed right afterwards
+      -- but it might make more sense once hidden is implemented
+      local row, _ = unpack(vim.api.nvim_win_get_cursor(win_id))
+      vim_options.callback(win_id, what[row])
+      vim.api.nvim_win_close(win_id, true)
+    end
+    vim.api.nvim_buf_set_keymap(
+      bufnr,
+      "n",
+      "<CR>",
+      '<cmd>lua require"popup".execute_callback(' .. bufnr .. ")<CR>",
+      { noremap = true }
+    )
+  end
+
+  -- TODO: Perhaps there's a way to return an object that looks like a window id,
+  --    but actually has some extra metadata about it.
+  --
+  --    This would make `hidden` a lot easier to manage
+  return win_id, {
+    win_id = win_id,
+    border = border,
+  }
+end
+
+function popup.execute_callback(bufnr)
+  if popup._callbacks[bufnr] then
+    local wrapper = popup._callbacks[bufnr]
+    wrapper()
+    popup._callbacks[bufnr] = nil
+  end
+end
+
+return popup
diff --git a/lua/plenary/popup/utils.lua b/lua/plenary/popup/utils.lua
new file mode 100644
index 0000000..e665e15
--- /dev/null
+++ b/lua/plenary/popup/utils.lua
@@ -0,0 +1,33 @@
+local utils = {}
+
+utils.bounded = function(value, min, max)
+  min = min or 0
+  max = max or math.huge
+
+  if min then
+    value = math.max(value, min)
+  end
+  if max then
+    value = math.min(value, max)
+  end
+
+  return value
+end
+
+utils.apply_defaults = function(original, defaults)
+  if original == nil then
+    original = {}
+  end
+
+  original = vim.deepcopy(original)
+
+  for k, v in pairs(defaults) do
+    if original[k] == nil then
+      original[k] = v
+    end
+  end
+
+  return original
+end
+
+return utils
diff --git a/lua/plenary/window/border.lua b/lua/plenary/window/border.lua
index d14a4a8..e74e2b8 100644
--- a/lua/plenary/window/border.lua
+++ b/lua/plenary/window/border.lua
@@ -12,6 +12,44 @@ Border._default_thickness = {
   left = 1,
 }
 
+local calc_left_start = function(title_pos, title_len, total_width)
+  if string.find(title_pos, "W") then
+    return 0
+  elseif string.find(title_pos, "E") then
+    return total_width - title_len
+  else
+    return math.floor((total_width - title_len) / 2)
+  end
+end
+
+local create_horizontal_line = function(title, pos, width, left_char, mid_char, right_char)
+  local title_len
+  if title == "" then
+    title_len = 0
+  else
+    local len = strings.strdisplaywidth(title)
+    local max_title_width = width - 2
+    if len > max_title_width then
+      title = strings.truncate(title, max_title_width)
+      len = strings.strdisplaywidth(title)
+    end
+    title = string.format(" %s ", title)
+    title_len = len + 2
+  end
+
+  local left_start = calc_left_start(pos, title_len, width)
+
+  local horizontal_line = string.format(
+    "%s%s%s%s%s",
+    left_char,
+    string.rep(mid_char, left_start),
+    title,
+    string.rep(mid_char, width - title_len - left_start),
+    right_char
+  )
+  return horizontal_line
+end
+
 function Border._create_lines(content_win_options, border_win_options)
   -- TODO: Handle border width, which I haven't right here.
   local thickness = border_win_options.border_thickness
@@ -28,35 +66,28 @@ function Border._create_lines(content_win_options, border_win_options)
   local topleft = (left_enabled and border_win_options.topleft) or ""
   local topright = (right_enabled and border_win_options.topright) or ""
 
+  -- border_win_options.title should have be a list with entries of the
+  -- form: { pos = foo, text = bar }.
+  -- pos can take values in { "NW", "N", "NE", "SW", "S", "SE" }
+  local titles = type(border_win_options.title) == "string" and { { pos = "N", text = border_win_options.title } }
+    or border_win_options.title
+    or {}
+
   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
-        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
+    for _, title in ipairs(titles) do
+      if string.find(title.pos, "N") then
+        topline = create_horizontal_line(
+          title.text,
+          title.pos,
+          content_win_options.width,
+          topleft,
+          border_win_options.top or "",
+          topright
+        )
+        break
       end
-
-      local midpoint = math.floor(content_win_options.width / 2)
-      local left_start = midpoint - math.ceil(title_len / 2)
-
-      topline = string.format(
-        "%s%s%s%s%s",
-        topleft,
-        string.rep(border_win_options.top, left_start),
-        title,
-        string.rep(border_win_options.top, content_win_options.width - title_len - left_start),
-        topright
-      )
-    else
+    end
+    if topline == nil then
       if top_enabled then
         topline = topleft .. string.rep(border_win_options.top, content_win_options.width) .. topright
       end
@@ -79,15 +110,28 @@ function Border._create_lines(content_win_options, border_win_options)
   end
 
   if bot_enabled then
-    table.insert(
-      border_lines,
-      string.format(
-        "%s%s%s",
-        (left_enabled and border_win_options.botleft) or "",
-        string.rep(border_win_options.bot, content_win_options.width),
-        (right_enabled and border_win_options.botright) or ""
-      )
-    )
+    local botline = nil
+    local botleft = (left_enabled and border_win_options.botleft) or ""
+    local botright = (right_enabled and border_win_options.botright) or ""
+    for _, title in ipairs(titles) do
+      if string.find(title.pos, "S") then
+        botline = create_horizontal_line(
+          title.text,
+          title.pos,
+          content_win_options.width,
+          botleft,
+          border_win_options.bot or "",
+          botright
+        )
+        break
+      end
+    end
+    if botline == nil then
+      if top_enabled then
+        botline = botleft .. string.rep(border_win_options.bot, content_win_options.width) .. botright
+      end
+    end
+    table.insert(border_lines, botline)
   end
 
   return border_lines
diff --git a/plenary.nvim-scm-1.rockspec b/plenary.nvim-scm-1.rockspec
new file mode 100644
index 0000000..027b3e6
--- /dev/null
+++ b/plenary.nvim-scm-1.rockspec
@@ -0,0 +1,103 @@
+local _MODREV, _SPECREV = 'scm', '-1'
+rockspec_format = "3.0"
+package = 'plenary.nvim'
+version = _MODREV .. _SPECREV
+
+description = {
+   summary = 'lua functions you don\'t want to write ',
+   labels = { "neovim" },
+   detailed = [[
+	  plenary: full; complete; entire; absolute; unqualified. All the lua functions I don't want to write twice.
+   ]],
+   homepage = 'http://github.com/nvim-lua/plenary.nvim',
+   license = 'MIT/X11',
+}
+
+dependencies = {
+   'lua >= 5.1, < 5.4',
+   'luassert'
+}
+
+source = {
+   url = 'http://github.com/nvim-lua/plenary.nvim/archive/v' .. _MODREV .. '.zip',
+   dir = 'plenary.nvim-' .. _MODREV,
+}
+
+if _MODREV == 'scm' then
+   source = {
+      url = 'git://github.com/nvim-lua/plenary.nvim',
+   }
+end
+
+build = {
+   type = 'builtin',
+   modules = {
+	   -- paths are relative to source.dir
+	   ["plenary.busted"] = "lua/plenary/busted.lua",
+	   ["plenary.class"] = "lua/plenary/class.lua",
+	   ["plenary.context_manager"] = "lua/plenary/context_manager.lua",
+	   ["plenary.debug_utils"] = "lua/plenary/debug_utils.lua",
+	   ["plenary.enum"] = "lua/plenary/enum.lua",
+	   ["plenary.errors"] = "lua/plenary/errors.lua",
+	   ["plenary.filetype"] = "lua/plenary/filetype.lua",
+	   ["plenary.fun"] = "lua/plenary/fun.lua",
+	   ["plenary.functional"] = "lua/plenary/functional.lua",
+	   ["plenary.init"] = "lua/plenary/init.lua",
+	   ["plenary.iterators"] = "lua/plenary/iterators.lua",
+	   ["plenary.job"] = "lua/plenary/job.lua",
+	   ["plenary.log"] = "lua/plenary/log.lua",
+	   ["plenary.nvim_meta"] = "lua/plenary/nvim_meta.lua",
+	   ["plenary.operators"] = "lua/plenary/operators.lua",
+	   ["plenary.path"] = "lua/plenary/path.lua",
+	   ["plenary.profile"] = "lua/plenary/profile.lua",
+	   ["plenary.reload"] = "lua/plenary/reload.lua",
+	   ["plenary.run"] = "lua/plenary/run.lua",
+	   ["plenary.scandir"] = "lua/plenary/scandir.lua",
+	   ["plenary.strings"] = "lua/plenary/strings.lua",
+	   ["plenary.tbl"] = "lua/plenary/tbl.lua",
+	   ["plenary.test_harness"] = "lua/plenary/test_harness.lua",
+
+	   ["plenary.async.api"] = "lua/plenary/async/api.lua",
+	   ["plenary.async.async"] = "lua/plenary/async/async.lua",
+	   ["plenary.async.control"] = "lua/plenary/async/control.lua",
+	   ["plenary.async.init"] = "lua/plenary/async/init.lua",
+	   ["plenary.async.lsp"] = "lua/plenary/async/lsp.lua",
+	   ["plenary.async.structs"] = "lua/plenary/async/structs.lua",
+	   ["plenary.async.tests"] = "lua/plenary/async/tests.lua",
+	   ["plenary.async.util"] = "lua/plenary/async/util.lua",
+	   ["plenary.async.uv_async"] = "lua/plenary/async/uv_async.lua",
+
+	   ["plenary.async_lib.api"] = "lua/plenary/async_lib/api.lua",
+	   ["plenary.async_lib.async"] = "lua/plenary/async_lib/async.lua",
+	   ["plenary.async_lib.init"] = "lua/plenary/async_lib/init.lua",
+	   ["plenary.async_lib.lsp"] = "lua/plenary/async_lib/lsp.lua",
+	   ["plenary.async_lib.structs"] = "lua/plenary/async_lib/structs.lua",
+	   ["plenary.async_lib.tests"] = "lua/plenary/async_lib/tests.lua",
+	   ["plenary.async_lib.util"] = "lua/plenary/async_lib/util.lua",
+	   ["plenary.async_lib.uv_async"] = "lua/plenary/async_lib/uv_async.lua",
+
+	   ["plenary.collections.py_list"] = "lua/plenary/collections/py_list.lua",
+
+	   ["plenary.lsp.override"] = "lua/plenary/lsp/override.lua",
+
+	   ["plenary.neorocks.init"] = "lua/plenary/neorocks/init.lua",
+
+	   ["plenary.popup.init"] = "lua/plenary/popup/init.lua",
+	   ["plenary.popup.utils"] = "lua/plenary/popup/utils.lua",
+
+	   ["plenary.profile.lua_profiler"] = "lua/plenary/profile/lua_profiler.lua",
+	   ["plenary.profile.memory_profiler"] = "lua/plenary/profile/memory_profiler.lua",
+	   ["plenary.profile.p"] = "lua/plenary/profile/p.lua",
+
+	   ["plenary.vararg.init"] = "lua/plenary/vararg/init.lua",
+	   ["plenary.vararg.rotate"] = "lua/plenary/vararg/rotate.lua",
+
+	   ["plenary.window.border"] = "lua/plenary/window/border.lua",
+	   ["plenary.window.float"] = "lua/plenary/window/float.lua",
+	   ["plenary.window.init"] = "lua/plenary/window/init.lua",
+   },
+   copy_directories = {
+	   'plugin'
+   }
+}
+


Test patch:
diff --git a/lua/plenary/async/tests.lua b/lua/plenary/async/tests.lua
index 5f72050..367b83d 100644
--- a/lua/plenary/async/tests.lua
+++ b/lua/plenary/async/tests.lua
@@ -1,4 +1,3 @@
-local a = require "plenary.async.async"
 local util = require "plenary.async.util"
 
 local M = {}
diff --git a/lua/plenary/test_harness.lua b/lua/plenary/test_harness.lua
index 3c0d188..0043bbc 100644
--- a/lua/plenary/test_harness.lua
+++ b/lua/plenary/test_harness.lua
@@ -18,15 +18,16 @@ local print_output = vim.schedule_wrap(function(_, ...)
   vim.cmd [[mode]]
 end)
 
-local nvim_output = vim.schedule_wrap(function(bufnr, ...)
-  if not vim.api.nvim_buf_is_valid(bufnr) then
-    return
-  end
-
-  for _, v in ipairs { ... } do
-    vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { v })
-  end
-end)
+local get_nvim_output = function(job_id)
+  return vim.schedule_wrap(function(bufnr, ...)
+    if not vim.api.nvim_buf_is_valid(bufnr) then
+      return
+    end
+    for _, v in ipairs { ... } do
+      vim.api.nvim_chan_send(job_id, v .. "\r\n")
+    end
+  end)
+end
 
 function harness.test_directory_command(command)
   local split_string = vim.split(command, " ")
@@ -39,14 +40,14 @@ end
 
 function harness.test_directory(directory, opts)
   print "Starting..."
-  opts = vim.tbl_deep_extend("force", { winopts = { winblend = 3 } }, opts or {})
+  opts = vim.tbl_deep_extend("force", { winopts = { winblend = 3 }, sequential = false, keep_going = true }, opts or {})
 
   local res = {}
   if not headless then
     res = win_float.percentage_range_window(0.95, 0.70, opts.winopts)
 
+    res.job_id = vim.api.nvim_open_term(res.bufnr, {})
     vim.api.nvim_buf_set_keymap(res.bufnr, "n", "q", ":q<CR>", {})
-    vim.api.nvim_buf_set_option(res.bufnr, "filetype", "terminal")
 
     vim.api.nvim_win_set_option(res.win_id, "winhl", "Normal:Normal")
     vim.api.nvim_win_set_option(res.win_id, "conceallevel", 3)
@@ -58,15 +59,14 @@ function harness.test_directory(directory, opts)
     vim.cmd "mode"
   end
 
-  local outputter = headless and print_output or nvim_output
+  local outputter = headless and print_output or get_nvim_output(res.job_id)
 
   local paths = harness._find_files_to_run(directory)
-  for _, p in ipairs(paths) do
-    outputter(res.bufnr, "Scheduling: " .. p.filename)
-  end
 
   local path_len = #paths
 
+  local failure = false
+
   local jobs = vim.tbl_map(function(p)
     local args = {
       "--headless",
@@ -83,7 +83,7 @@ function harness.test_directory(directory, opts)
       table.insert(args, opts.minimal_init)
     end
 
-    return Job:new {
+    local job = Job:new {
       command = vim.v.progpath,
       args = args,
 
@@ -109,12 +109,25 @@ function harness.test_directory(directory, opts)
         vim.cmd "mode"
       end),
     }
+    job.nvim_busted_path = p.filename
+    return job
   end, paths)
 
   log.debug "Running..."
   for i, j in ipairs(jobs) do
+    outputter(res.bufnr, "Scheduling: " .. j.nvim_busted_path)
     j:start()
-    log.debug("... Completed job number", i)
+    if opts.sequential then
+      log.debug("... Sequential wait for job number", i)
+      Job.join(j, 50000)
+      log.debug("... Completed job number", i)
+      if j.code ~= 0 then
+        failure = true
+        if not opts.keep_going then
+          break
+        end
+      end
+    end
   end
 
   -- TODO: Probably want to let people know when we've completed everything.
@@ -122,17 +135,20 @@ function harness.test_directory(directory, opts)
     return
   end
 
-  table.insert(jobs, 50000)
-  log.debug "...Waiting"
-  Job.join(unpack(jobs))
-  table.remove(jobs, table.getn(jobs))
+  if not opts.sequential then
+    table.insert(jobs, 50000)
+    log.debug "... Parallel wait"
+    Job.join(unpack(jobs))
+    log.debug "... Completed jobs"
+    table.remove(jobs, table.getn(jobs))
+    failure = f.any(function(_, v)
+      return v.code ~= 0
+    end, jobs)
+  end
   vim.wait(100)
-  log.debug "Done..."
 
   if headless then
-    if f.any(function(_, v)
-      return v.code ~= 0
-    end, jobs) then
+    if failure then
       os.exit(1)
     end
 
diff --git a/tests/plenary/job_spec.lua b/tests/plenary/job_spec.lua
index 329a118..cc0b20b 100644
--- a/tests/plenary/job_spec.lua
+++ b/tests/plenary/job_spec.lua
@@ -72,7 +72,7 @@ describe("Job", function()
       assert.are.same(job:result(), results)
     end)
 
-    it("should split stdin across newlines with no ending newline", function()
+    pending("should split stdin across newlines with no ending newline", function()
       local results = {}
       local job = Job:new {
         -- writer = "hello\nword\nthis is\ntj",
@@ -91,6 +91,42 @@ describe("Job", function()
       assert.are.same(job:result(), { "hello", "world" })
       assert.are.same(job:result(), results)
     end)
+
+    it("should return last line when there is ending newline", function()
+      local results = {}
+      local job = Job:new {
+        command = "echo",
+
+        args = { "-e", "test1\ntest2" },
+
+        on_stdout = function(_, data)
+          table.insert(results, data)
+        end,
+      }
+
+      job:sync()
+
+      assert.are.same(job:result(), { "test1", "test2" })
+      assert.are.same(job:result(), results)
+    end)
+
+    pending("should return last line when there is no ending newline", function()
+      local results = {}
+      local job = Job:new {
+        command = "echo",
+
+        args = { "-en", "test1\ntest2" },
+
+        on_stdout = function(_, data)
+          table.insert(results, data)
+        end,
+      }
+
+      job:sync()
+
+      assert.are.same(job:result(), { "test1", "test2" })
+      assert.are.same(job:result(), results)
+    end)
   end)
 
   describe("env", function()
diff --git a/tests/plenary/json_spec.lua b/tests/plenary/json_spec.lua
new file mode 100644
index 0000000..955668a
--- /dev/null
+++ b/tests/plenary/json_spec.lua
@@ -0,0 +1,70 @@
+local json = require "plenary.json"
+local eq = assert.are.same
+
+describe("json", function()
+  it("replace comments with whitespace", function()
+    eq(json.json_strip_comments '//comment\n{"a":"b"}', '         \n{"a":"b"}')
+    eq(json.json_strip_comments '/*//comment*/{"a":"b"}', '             {"a":"b"}')
+    eq(json.json_strip_comments '{"a":"b"//comment\n}', '{"a":"b"         \n}')
+    eq(json.json_strip_comments '{"a":"b"/*comment*/}', '{"a":"b"           }')
+    eq(json.json_strip_comments '{"a"/*\n\n\ncomment\r\n*/:"b"}', '{"a"  \n\n\n       \r\n  :"b"}')
+    eq(json.json_strip_comments '/*!\n * comment\n */\n{"a":"b"}', '   \n          \n   \n{"a":"b"}')
+    eq(json.json_strip_comments '{/*comment*/"a":"b"}', '{           "a":"b"}')
+  end)
+
+  it("remove comments", function()
+    local options = { whitespace = false }
+    eq(json.json_strip_comments('//comment\n{"a":"b"}', options), '\n{"a":"b"}')
+    eq(json.json_strip_comments('/*//comment*/{"a":"b"}', options), '{"a":"b"}')
+    eq(json.json_strip_comments('{"a":"b"//comment\n}', options), '{"a":"b"\n}')
+    eq(json.json_strip_comments('{"a":"b"/*comment*/}', options), '{"a":"b"}')
+    eq(json.json_strip_comments('{"a"/*\n\n\ncomment\r\n*/:"b"}', options), '{"a":"b"}')
+    eq(json.json_strip_comments('/*!\n * comment\n */\n{"a":"b"}', options), '\n{"a":"b"}')
+    eq(json.json_strip_comments('{/*comment*/"a":"b"}', options), '{"a":"b"}')
+  end)
+
+  it("doesn't strip comments inside strings", function()
+    eq(json.json_strip_comments '{"a":"b//c"}', '{"a":"b//c"}')
+    eq(json.json_strip_comments '{"a":"b/*c*/"}', '{"a":"b/*c*/"}')
+    eq(json.json_strip_comments '{"/*a":"b"}', '{"/*a":"b"}')
+    eq(json.json_strip_comments '{"\\"/*a":"b"}', '{"\\"/*a":"b"}')
+  end)
+
+  it("consider escaped slashes when checking for escaped string quote", function()
+    eq(json.json_strip_comments '{"\\\\":"https://foobar.com"}', '{"\\\\":"https://foobar.com"}')
+    eq(json.json_strip_comments '{"foo\\"":"https://foobar.com"}', '{"foo\\"":"https://foobar.com"}')
+  end)
+
+  it("line endings - no comments", function()
+    eq(json.json_strip_comments '{"a":"b"\n}', '{"a":"b"\n}')
+    eq(json.json_strip_comments '{"a":"b"\r\n}', '{"a":"b"\r\n}')
+  end)
+
+  it("line endings - single line comment", function()
+    eq(json.json_strip_comments '{"a":"b"//c\n}', '{"a":"b"   \n}')
+    eq(json.json_strip_comments '{"a":"b"//c\r\n}', '{"a":"b"   \r\n}')
+  end)
+
+  it("line endings - single line block comment", function()
+    eq(json.json_strip_comments '{"a":"b"/*c*/\n}', '{"a":"b"     \n}')
+    eq(json.json_strip_comments '{"a":"b"/*c*/\r\n}', '{"a":"b"     \r\n}')
+  end)
+
+  it("line endings - multi line block comment", function()
+    eq(json.json_strip_comments '{"a":"b",/*c\nc2*/"x":"y"\n}', '{"a":"b",   \n    "x":"y"\n}')
+    eq(json.json_strip_comments '{"a":"b",/*c\r\nc2*/"x":"y"\r\n}', '{"a":"b",   \r\n    "x":"y"\r\n}')
+  end)
+
+  it("line endings - works at EOF", function()
+    local options = { whitespace = false }
+    eq(json.json_strip_comments '{\r\n\t"a":"b"\r\n} //EOF', '{\r\n\t"a":"b"\r\n}      ')
+    eq(json.json_strip_comments('{\r\n\t"a":"b"\r\n} //EOF', options), '{\r\n\t"a":"b"\r\n} ')
+  end)
+
+  it("handles weird escaping", function()
+    eq(
+      json.json_strip_comments [[{"x":"x \"sed -e \\\"s/^.\\\\{46\\\\}T//\\\" -e \\\"s/#033/\\\\x1b/g\\\"\""}]],
+      [[{"x":"x \"sed -e \\\"s/^.\\\\{46\\\\}T//\\\" -e \\\"s/#033/\\\\x1b/g\\\"\""}]]
+    )
+  end)
+end)
diff --git a/tests/plenary/popup_requires_spec.lua b/tests/plenary/popup_requires_spec.lua
new file mode 100644
index 0000000..2909f65
--- /dev/null
+++ b/tests/plenary/popup_requires_spec.lua
@@ -0,0 +1,42 @@
+local scandir = require "plenary.scandir"
+local Path = require "plenary.path"
+
+local eq = assert.are.same
+
+describe("plenary.popup", function()
+  local allowed_imports = {
+    ["plenary.window"] = true,
+    ["plenary.window.border"] = true,
+  }
+
+  local matches_any_import = function(line)
+    local matched = string.match(line, [[require."(.*)"]])
+    if matched and not vim.startswith(matched, "plenary.popup") then
+      if not allowed_imports[matched] then
+        return true, string.format("Not an allowed import for popup: %s. Line: %s", matched, line)
+      end
+    end
+
+    return false, nil
+  end
+
+  -- Tests to make sure that we're matching both types of requires
+  it("should match these kinds of patterns", function()
+    eq(true, matches_any_import [[local x = require "plenary.other"]])
+    eq(true, matches_any_import [[local x = require("plenary.module").something]])
+  end)
+
+  it("must not require anything other than Window and Border from plenary", function()
+    local result = scandir.scan_dir("./lua/plenary/popup", { depth = 1 })
+
+    for _, file in ipairs(result) do
+      local popup_file = Path:new(file)
+      local lines = popup_file:readlines()
+
+      for _, line in ipairs(lines) do
+        local matches, msg = matches_any_import(line)
+        eq(false, matches, msg)
+      end
+    end
+  end)
+end)
diff --git a/tests/plenary/popup_spec.lua b/tests/plenary/popup_spec.lua
new file mode 100644
index 0000000..fecf14e
--- /dev/null
+++ b/tests/plenary/popup_spec.lua
@@ -0,0 +1,132 @@
+local popup = require "plenary.popup"
+
+local eq = assert.are.same
+
+describe("plenary.popup", function()
+  before_each(function()
+    vim.cmd [[highlight PopupColor1 ctermbg=lightblue guibg=lightblue]]
+    vim.cmd [[highlight PopupColor2 ctermbg=lightcyan guibg=lightcyan]]
+  end)
+
+  -- TODO: Probably want to clear all the popups between iterations
+  -- after_each(function() end)
+
+  it("can create a very simple window", function()
+    local win_id = popup.create("hello there", {
+      line = 1,
+      col = 1,
+      width = 20,
+    })
+
+    local win_config = vim.api.nvim_win_get_config(win_id)
+    eq(20, win_config.width)
+  end)
+
+  it("can apply a highlight", function()
+    local win_id = popup.create("hello there", {
+      highlight = "PopupColor1",
+    })
+
+    eq("Normal:PopupColor1", vim.api.nvim_win_get_option(win_id, "winhl"))
+  end)
+
+  it("can create a border", function()
+    local win_id, config = popup.create("hello border", {
+      line = 2,
+      col = 3,
+      border = {},
+    })
+
+    eq(true, vim.api.nvim_win_is_valid(win_id))
+
+    local border_id = config.border.win_id
+    assert(border_id, "Has a border win id")
+    eq(true, vim.api.nvim_win_is_valid(border_id))
+  end)
+
+  it("can do basic padding", function()
+    local win_id = popup.create("12345", {
+      line = 1,
+      col = 1,
+      padding = {},
+    })
+
+    local bufnr = vim.api.nvim_win_get_buf(win_id)
+    eq({ "", " 12345 ", "" }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false))
+  end)
+
+  it("can do padding and border", function()
+    local win_id, config = popup.create("hello border", {
+      line = 2,
+      col = 2,
+      border = {},
+      padding = {},
+    })
+
+    local bufnr = vim.api.nvim_win_get_buf(win_id)
+    eq({ "", " hello border ", "" }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false))
+
+    local border_id = config.border.win_id
+    assert(border_id, "Has a border win id")
+    eq(true, vim.api.nvim_win_is_valid(border_id))
+  end)
+
+  describe("borderchars", function()
+    local test_border = function(name, borderchars, expected)
+      it(name, function()
+        local win_id, config = popup.create("all the plus signs", {
+          line = 8,
+          col = 55,
+          padding = { 0, 3, 0, 3 },
+          borderchars = borderchars,
+        })
+
+        local border_id = config.border.win_id
+        local border_bufnr = vim.api.nvim_win_get_buf(border_id)
+        eq(expected, vim.api.nvim_buf_get_lines(border_bufnr, 0, -1, false))
+      end)
+    end
+
+    test_border("can support multiple border patterns", { "+" }, {
+      "++++++++++++++++++++++++++",
+      "+                        +",
+      "++++++++++++++++++++++++++",
+    })
+
+    test_border("can support multiple patterns inside the borderchars", { "-", "+" }, {
+      "+------------------------+",
+      "-                        -",
+      "+------------------------+",
+    })
+  end)
+
+  describe("what", function()
+    it("can be an existing bufnr", function()
+      local bufnr = vim.api.nvim_create_buf(false, false)
+      vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { "pass bufnr 1", "pass bufnr 2" })
+      local win_id = popup.create(bufnr, {
+        line = 8,
+        col = 55,
+        minwidth = 20,
+      })
+
+      eq(bufnr, vim.api.nvim_win_get_buf(win_id))
+      eq({ "pass bufnr 1", "pass bufnr 2" }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false))
+    end)
+  end)
+
+  describe("cursor", function()
+    pending("not yet tested", function()
+      popup.create({ "option 1", "option 2" }, {
+        line = "cursor+2",
+        col = "cursor+2",
+        border = { 1, 1, 1, 1 },
+        enter = true,
+        cursorline = true,
+        callback = function(win_id, sel)
+          print(sel)
+        end,
+      })
+    end)
+  end)
+end)