Skip to content

fix(util): Path.parent now works on windows (#1168) #1180

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/luasnip.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
*luasnip.txt* For NVIM v0.8.0 Last change: 2024 May 18
*luasnip.txt* For NVIM v0.8.0 Last change: 2024 May 22

==============================================================================
Table of Contents *luasnip-table-of-contents*
Expand Down
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)