Repository: nvim-lua/plenary.nvim
Pull request #131 (2021-04-09 14:17:26)

Replace f.map with vim.tbl_map
## Avoid shadowing `f` in functional.lua

`fun` is more fun than `f`

## Replace f.map with vim.tbl_map

The implementations are slightly different (`vim.tbl_map` preserves the keys,
and doesn't do an implicit transformation to a list like table)

The `iter` argument name to me also implied that it would take a function
following the lua iterator protocol but that wasn't the case.

Motivation was mostly that I asked myself what's special about `f.map`  while reading some test_harness code

(Not sure if this is okay, as it breaks the API?)

oberblastmeister: I'm not sure that we care that the keys are preserved because we are using a list like table anyway. Also, `plenary/functional.lua` is largely deprecated because they only work with tables and have to reallocate each time and is also confusing because they don't work with lua's iterator protocol. You should just use `plenary/iterators.lua` instead.

Patch:
diff --git a/README.md b/README.md
index 458b1ca..d6694d6 100644
--- a/README.md
+++ b/README.md
@@ -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
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/async_lib/util.lua b/lua/plenary/async_lib/util.lua
index b0d035c..d719c7d 100644
--- a/lua/plenary/async_lib/util.lua
+++ b/lua/plenary/async_lib/util.lua
@@ -127,9 +127,9 @@ end
 ---when a permit can be acquired returns it
 ---call permit:forget() to forget the permit
 Semaphore.acquire = a.wrap(function(self, callback)
-  self.permits = self.permits - 1
-
-  if self.permits <= 0 then
+  if self.permits > 0 then
+    self.permits = self.permits - 1
+  else
     table.insert(self.handles, callback)
     return
   end
@@ -140,9 +140,9 @@ Semaphore.acquire = a.wrap(function(self, callback)
     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)
-      self.permits = self.permits - 1
     end
   end
 
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/log.lua b/lua/plenary/log.lua
index e10e963..48798f3 100644
--- a/lua/plenary/log.lua
+++ b/lua/plenary/log.lua
@@ -51,7 +51,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
diff --git a/lua/plenary/path.lua b/lua/plenary/path.lua
index 8d7376d..2de31c2 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
 
@@ -266,11 +300,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 +341,15 @@ local shorten = (function()
     end
   end
   return function(filename)
-    return filename
+    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
 
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


Test patch:
diff --git a/lua/plenary/test_harness.lua b/lua/plenary/test_harness.lua
index 1c4bf46..35caf63 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',
@@ -83,7 +83,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 +145,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/tests/plenary/async_lib/semaphore_spec.lua b/tests/plenary/async_lib/semaphore_spec.lua
index f2908fb..f3df6a4 100644
--- a/tests/plenary/async_lib/semaphore_spec.lua
+++ b/tests/plenary/async_lib/semaphore_spec.lua
@@ -12,6 +12,47 @@ a.describe('semaphore', function()
     eq(status, false)
   end)
 
-  a.it('should count properly', function()
+  a.it('should acquire a permit if available', function ()
+    local sem = a.util.Semaphore.new(1)
+    local permit = await(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 completed = false
+    local blocking = async(function ()
+      await(sem:acquire())
+      completed = true
+    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())
+    permit:forget()
+    local next_permit = await(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 completed = false
+    local blocking = async(function ()
+      await(sem:acquire())
+      completed = true
+    end)
+
+    a.run(blocking())
+    permit:forget()
+
+    eq(completed, true)
   end)
 end)
diff --git a/tests/plenary/path_spec.lua b/tests/plenary/path_spec.lua
index 4eb509a..571d89f 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()
@@ -153,16 +158,48 @@ describe('Path', function()
 
   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/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)