Skip to content

Commit

Permalink
fix(util): Path.parent now works on windows (#1168)
Browse files Browse the repository at this point in the history
On Windows, both forward slash `/` and backslash `\\` work as the path
separator. Linux and MacOS can have backslash as a valid filename
character.

Unit tests are also provided for each platform because `Path.parent`
depends on the local variable `sep` which depends on `jit.os`.

Fixes #1168
  • Loading branch information
xudyang1 committed May 22, 2024
1 parent de1a287 commit 4a8e17a
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 6 deletions.
70 changes: 64 additions & 6 deletions lua/luasnip/util/path.lua
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,72 @@ function Path.components(path)
return vim.split(path, sep, { plain = true, trimempty = true })
end

function Path.parent(path)
local last_component = path:match("%" .. sep .. "[^" .. sep .. "]+$")
if not last_component then
return nil
---Get parent of a path, without trailing separator
---if path is a directory or does not have a parent, returns nil
---Example:
--- On platforms that use "\\" backslash as path separator, e.g., Windows:
--- Path.parent("C:/project_root/file.txt") -- returns "C:/project_root"
--- Path.parent([[C:\project_root\file.txt]]) -- returns [[C:\project_root]]
---
--- -- the followings return `nil`s
--- Path.parent("C:/")
--- Path.parent([[C:\]])
--- Path.parent([[C:\project_root\]])
---
--- -- WARN: although it's unlikely that we will reach the driver's root
--- -- level, Path.parent("C:\file.txt") returns "C:", and please be
--- -- cautious when passing the parent path to some vim functions because
--- -- some vim functions on Windows treat "C:" as a file instead:
--- -- vim.fn.fnamemodify("C:", ":p") -- returns $CWD .. sep .. "C:"
--- -- To get the desired result, use vim.fn.fnamemodify("C:" .. sep, ":p")
---
--- On platforms that use "/" forward slash as path separator, e.g., linux:
--- Path.parent("/project_root/file.txt") returns "/project_root"
--- Path.parent("/file.txt") returns ""
---
--- -- the followings return `nil`s
--- Path.parent("/")
--- Path.parent("/project_root/")
---
--- -- backslash in a valid filename character in linux:
--- Path.parent([[/project_root/\valid\file\name.txt]]) returns "/project_root"
Path.parent = (function()
---@alias PathSeparator "/" | "\\"
---@param os_sep PathSeparator
---@return fun(string): string | nil
local function generate_parent(os_sep)
if os_sep == "/" then
---@param path string
---@return string | nil
return function(path)
local last_component = path:match("[/]+[^/]+$")
if not last_component then
return nil
end

return path:sub(1, #path - #last_component)
end
else
---@param path string
---@return string | nil
return function(path)
local last_component = path:match("[/\\]+[^/\\]+$")
if not last_component then
return nil
end

return path:sub(1, #path - #last_component)
end
end
end

return path:sub(1, #path - #last_component)
end
-- for test only
if __LUASNIP_TEST_SEP_OVERRIDE then
return generate_parent
else
return generate_parent(sep)
end
end)()

-- returns nil if the file does not exist!
Path.normalize = uv.fs_realpath
Expand Down
110 changes: 110 additions & 0 deletions tests/unit/utils_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,113 @@ describe("luasnip.util.str:dedent", function()
check("2 and 1", " one\n two", " one\ntwo")
check("2 and 2", " one\n two", "one\ntwo")
end)

describe("luasnip.util.Path.parent", function()
local function assert_parents(separator, examples)
for _, example in ipairs(examples) do
if example.expect then
it(example.path, function()
assert.are.same(
example.expect,
exec_lua(
"__LUASNIP_TEST_SEP_OVERRIDE = [["
.. separator
.. "]] "
.. 'return require("luasnip.util.path").parent([['
.. separator
.. "]])([["
.. example.path
.. "]])"
)
)
end)
else
it(example.path .. " to be nil", function()
assert.is_true(
exec_lua(
"__LUASNIP_TEST_SEP_OVERRIDE = [["
.. separator
.. "]] "
.. 'return require("luasnip.util.path").parent([['
.. separator
.. "]])([["
.. example.path
.. "]]) == nil"
)
)
end)
end
end
end

describe("backslash as the path separator", function()
local examples = {
{
path = [[C:\Users\username\AppData\Local\nvim-data\log]],
expect = [[C:\Users\username\AppData\Local\nvim-data]],
},
{
path = [[C:/Users/username/AppData/Local/nvim-data/log]],
expect = [[C:/Users/username/AppData/Local/nvim-data]],
},
{
path = [[D:\Projects\project_folder\source_code.py]],
expect = [[D:\Projects\project_folder]],
},
{
path = [[D:/Projects/project_folder/source_code.py]],
expect = [[D:/Projects/project_folder]],
},
{ path = [[E:\Music\\\\]], expect = nil },
{ path = [[E:/Music////]], expect = nil },
{ path = [[E:\\Music\\\\]], expect = nil },
{ path = [[E://Music////]], expect = nil },
{ path = [[F:\]], expect = nil },
{ path = [[F:\\]], expect = nil },
{ path = [[F:/]], expect = nil },
{ path = [[F://]], expect = nil },
}

assert_parents("\\", examples)
end)

describe("forward slash as the path separator", function()
local examples = {
{
path = [[/home/usuario/documents/archivo.txt]],
expect = [[/home/usuario/documents]],
},
{
path = [[/var/www/html////index.html]],
expect = [[/var/www/html]],
},
{
path = [[/mnt/backup/backup_file.tar.gz]],
expect = [[/mnt/backup]],
},
{
path = [[/mnt/]],
expect = nil,
},
{
path = [[/mnt////]],
expect = nil,
},
{
path = [[/project/\backslash\is\legal\in\linux\filename.txt]],
expect = [[/project]],
},
{
path = [[/\\\\]],
expect = "",
},
{
path = [[/\\\\////]],
expect = nil,
},
{ path = [[/]], expect = nil },
}

assert_parents("/", examples)
end)
end)

0 comments on commit 4a8e17a

Please sign in to comment.