Skip to content

Commit 39766a2

Browse files
committed
fix(util): Path.parent now works on windows (#1168)
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
1 parent de1a287 commit 39766a2

File tree

2 files changed

+176
-6
lines changed

2 files changed

+176
-6
lines changed

lua/luasnip/util/path.lua

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,72 @@ function Path.components(path)
193193
return vim.split(path, sep, { plain = true, trimempty = true })
194194
end
195195

196-
function Path.parent(path)
197-
local last_component = path:match("%" .. sep .. "[^" .. sep .. "]+$")
198-
if not last_component then
199-
return nil
196+
---Get parent of a path, without trailing separator
197+
---if path is a directory or does not have a parent, returns nil
198+
---Example:
199+
--- On platforms that use "\\" backslash as path separator, e.g., Windows:
200+
--- Path.parent("C:/project_root/file.txt") -- returns "C:/project_root"
201+
--- Path.parent([[C:\project_root\file.txt]]) -- returns [[C:\project_root]]
202+
---
203+
--- -- the followings return `nil`s
204+
--- Path.parent("C:/")
205+
--- Path.parent([[C:\]])
206+
--- Path.parent([[C:\project_root\]])
207+
---
208+
--- -- WARN: although it's unlikely that we will reach the driver's root
209+
--- -- level, Path.parent("C:\file.txt") returns "C:", and please be
210+
--- -- cautious when passing the parent path to some vim functions because
211+
--- -- some vim functions on Windows treat "C:" as a file instead:
212+
--- -- vim.fn.fnamemodify("C:", ":p") -- returns $CWD .. sep .. "C:"
213+
--- -- To get the desired result, use vim.fn.fnamemodify("C:" .. sep, ":p")
214+
---
215+
--- On platforms that use "/" forward slash as path separator, e.g., linux:
216+
--- Path.parent("/project_root/file.txt") returns "/project_root"
217+
--- Path.parent("/file.txt") returns ""
218+
---
219+
--- -- the followings return `nil`s
220+
--- Path.parent("/")
221+
--- Path.parent("/project_root/")
222+
---
223+
--- -- backslash in a valid filename character in linux:
224+
--- Path.parent([[/project_root/\valid\file\name.txt]]) returns "/project_root"
225+
Path.parent = (function()
226+
---@alias PathSeparator "/" | "\\"
227+
---@param os_sep PathSeparator
228+
---@return fun(string): string | nil
229+
local function generate_parent(os_sep)
230+
if os_sep == "/" then
231+
---@param path string
232+
---@return string | nil
233+
return function(path)
234+
local last_component = path:match("[/]+[^/]+$")
235+
if not last_component then
236+
return nil
237+
end
238+
239+
return path:sub(1, #path - #last_component)
240+
end
241+
else
242+
---@param path string
243+
---@return string | nil
244+
return function(path)
245+
local last_component = path:match("[/\\]+[^/\\]+$")
246+
if not last_component then
247+
return nil
248+
end
249+
250+
return path:sub(1, #path - #last_component)
251+
end
252+
end
200253
end
201254

202-
return path:sub(1, #path - #last_component)
203-
end
255+
-- for test only
256+
if __LUASNIP_TEST_SEP_OVERRIDE then
257+
return generate_parent
258+
else
259+
return generate_parent(sep)
260+
end
261+
end)()
204262

205263
-- returns nil if the file does not exist!
206264
Path.normalize = uv.fs_realpath

tests/unit/utils_spec.lua

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,115 @@ describe("luasnip.util.str:dedent", function()
2323
check("2 and 1", " one\n two", " one\ntwo")
2424
check("2 and 2", " one\n two", "one\ntwo")
2525
end)
26+
27+
describe("luasnip.util.Path.parent", function()
28+
local function assert_parents(separator, examples)
29+
for _, example in ipairs(examples) do
30+
if example.expect then
31+
it(example.path, function()
32+
assert.are.same(
33+
example.expect,
34+
exec_lua(
35+
"__LUASNIP_TEST_SEP_OVERRIDE = [["
36+
.. separator
37+
.. "]] "
38+
.. 'return require("luasnip.util.path").parent([['
39+
.. separator
40+
.. "]])([["
41+
.. example.path
42+
.. "]]) == [["
43+
.. example.expect
44+
.. "]]"
45+
)
46+
)
47+
end)
48+
else
49+
it(example.path .. " to be nil", function()
50+
assert.is_true(
51+
exec_lua(
52+
"__LUASNIP_TEST_SEP_OVERRIDE = [["
53+
.. separator
54+
.. "]] "
55+
.. 'return require("luasnip.util.path").parent([['
56+
.. separator
57+
.. "]])([["
58+
.. example.path
59+
.. "]]) == nil"
60+
)
61+
)
62+
end)
63+
end
64+
end
65+
end
66+
67+
describe("backslash as the path separator", function()
68+
local examples = {
69+
{
70+
path = [[C:\Users\username\AppData\Local\nvim-data\log]],
71+
expect = [[C:\Users\username\AppData\Local\nvim-data]],
72+
},
73+
{
74+
path = [[C:/Users/username/AppData/Local/nvim-data/log]],
75+
expect = [[C:/Users/username/AppData/Local/nvim-data]],
76+
},
77+
{
78+
path = [[D:\Projects\project_folder\source_code.py]],
79+
expect = [[D:\Projects\project_folder]],
80+
},
81+
{
82+
path = [[D:/Projects/project_folder/source_code.py]],
83+
expect = [[D:/Projects/project_folder]],
84+
},
85+
{ path = [[E:\Music\\\\]], expect = nil },
86+
{ path = [[E:/Music////]], expect = nil },
87+
{ path = [[E:\\Music\\\\]], expect = nil },
88+
{ path = [[E://Music////]], expect = nil },
89+
{ path = [[F:\]], expect = nil },
90+
{ path = [[F:\\]], expect = nil },
91+
{ path = [[F:/]], expect = nil },
92+
{ path = [[F://]], expect = nil },
93+
}
94+
95+
assert_parents("\\", examples)
96+
end)
97+
98+
describe("forward slash as the path separator", function()
99+
local examples = {
100+
{
101+
path = [[/home/usuario/documents/archivo.txt]],
102+
expect = [[/home/usuario/documents]],
103+
},
104+
{
105+
path = [[/var/www/html////index.html]],
106+
expect = [[/var/www/html]],
107+
},
108+
{
109+
path = [[/mnt/backup/backup_file.tar.gz]],
110+
expect = [[/mnt/backup]],
111+
},
112+
{
113+
path = [[/mnt/]],
114+
expect = nil,
115+
},
116+
{
117+
path = [[/mnt////]],
118+
expect = nil,
119+
},
120+
{
121+
path = [[/project/\backslash\is\legal\in\linux\filename.txt]],
122+
expect = [[/project]],
123+
},
124+
{
125+
path = [[/\\\\]],
126+
expect = "",
127+
},
128+
{
129+
path = [[/\\\\////]],
130+
expect = nil,
131+
},
132+
{ path = [[/]], expect = nil },
133+
}
134+
135+
assert_parents("/", examples)
136+
end)
137+
end)

0 commit comments

Comments
 (0)