Skip to content

Commit 32ddebb

Browse files
committed
update Path instantiation and path parsing
1 parent f4072c4 commit 32ddebb

File tree

2 files changed

+66
-88
lines changed

2 files changed

+66
-88
lines changed

lua/plenary/path2.lua

Lines changed: 61 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
--- to this. Allows us to compute `filename` from "metadata" parsed once on
1919
--- instantiation.
2020

21-
--- TODO: rework `split_root` logic according to python 3.12
2221
--- TODO: rework `_filename` according to `_format_parsed_parts`
2322

2423
local uv = vim.loop
@@ -49,76 +48,46 @@ function _WindowsPath:convert_altsep(p)
4948
return (p:gsub(self.altsep, self.sep))
5049
end
5150

52-
---@param part string path
51+
---@param part string path with only `\` separators
5352
---@return string drv
5453
---@return string root
5554
---@return string relpath
5655
function _WindowsPath:split_root(part)
5756
-- https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
58-
local prefix = ""
57+
local unc_prefix = "\\\\?\\UNC\\"
5958
local first, second = part:sub(1, 1), part:sub(2, 2)
6059

61-
if first == self.sep and second == self.sep then
62-
prefix, part = self:_split_extended_path(part)
63-
first, second = part:sub(1, 1), part:sub(2, 2)
64-
end
65-
66-
local third = part:sub(3, 3)
67-
68-
if first == self.sep and second == self.sep and third ~= self.sep then
69-
-- is a UNC path:
70-
-- vvvvvvvvvvvvvvvvvvvvv root
71-
-- \\machine\mountpoint\directory\etc\...
72-
-- directory ^^^^^^^^^^^^^^
60+
if first == self.sep then
61+
if second == self.sep then
62+
-- UNC drives, e.g. \\server\share or \\?\UNC\server\share
63+
-- Device drives, e.g. \\.\device or \\?\device
64+
local start = part:sub(1, 8):upper() == unc_prefix and 8 or 2
65+
local index = part:find(self.sep, start)
66+
if index == nil then
67+
return part, "", "" -- paths only has drive info
68+
end
7369

74-
local index = part:find(self.sep, 3)
75-
if index ~= nil then
7670
local index2 = part:find(self.sep, index + 1)
77-
if index2 ~= index + 1 then
78-
if index2 == nil then
79-
index2 = #part
80-
end
81-
82-
if prefix ~= "" then
83-
return prefix + part:sub(2, index2 - 1), self.sep, part:sub(index2 + 1)
84-
else
85-
return part:sub(1, index2 - 1), self.sep, part:sub(index2 + 1)
86-
end
71+
if index2 == nil then
72+
return part, "", "" -- still paths only has drive info
8773
end
74+
return part:sub(1, index2 - 1), self.sep, part:sub(index2 + 1)
75+
else
76+
-- Relative path with root, eg. \Windows
77+
return "", part:sub(1, 1), part:sub(2)
8878
end
89-
end
90-
91-
local drv, root = "", ""
92-
if second == ":" and first:match "%a" then
93-
drv, part = part:sub(1, 2), part:sub(3)
94-
first = third
95-
end
96-
97-
if first == self.sep then
98-
root = first
99-
part = part:gsub("^" .. self.sep .. "+", "")
100-
end
101-
102-
return prefix .. drv, root, part
103-
end
104-
105-
---@param p string path
106-
---@return string
107-
---@return string
108-
function _WindowsPath:_split_extended_path(p)
109-
local ext_prefix = [[\\?\]]
110-
local prefix = ""
111-
112-
if p:sub(1, #ext_prefix) == ext_prefix then
113-
prefix = p:sub(1, 4)
114-
p = p:sub(5)
115-
if p:sub(1, 3) == "UNC" .. self.sep then
116-
prefix = prefix .. p:sub(1, 3)
117-
p = self.sep .. p:sub(4)
79+
elseif part:sub(2, 2) == ":" then
80+
if part:sub(3, 3) == self.sep then
81+
-- absolute path with drive, eg. C:\Windows
82+
return part:sub(1, 2), self.sep, part:sub(3)
83+
else
84+
-- relative path with drive, eg. C:Windows
85+
return part:sub(1, 2), "", part:sub(3)
11886
end
87+
else
88+
-- relative path, eg. Windows
89+
return "", "", part
11990
end
120-
121-
return prefix, p
12291
end
12392

12493
---@class plenary._PosixPath : plenary._Path
@@ -206,21 +175,21 @@ path.root = (function()
206175
end)()
207176

208177
---@param parts string[]
209-
---@param _path plenary._Path
178+
---@param _flavor plenary._Path
210179
---@return string drv
211180
---@return string root
212181
---@return string[]
213-
local function parse_parts(parts, _path)
182+
local function parse_parts(parts, _flavor)
214183
local drv, root, rel, parsed = "", "", "", {}
215184

216185
for i = #parts, 1, -1 do
217186
local part = parts[i]
218-
part = _path:convert_altsep(part)
187+
part = _flavor:convert_altsep(part)
219188

220-
drv, root, rel = _path:split_root(part)
189+
drv, root, rel = _flavor:split_root(part)
221190

222-
if rel:match(_path.sep) then
223-
local relparts = vim.split(rel, _path.sep)
191+
if rel:match(_flavor.sep) then
192+
local relparts = vim.split(rel, _flavor.sep)
224193
for j = #relparts, 1, -1 do
225194
local p = relparts[j]
226195
if p ~= "" and p ~= "." then
@@ -233,19 +202,18 @@ local function parse_parts(parts, _path)
233202
end
234203
end
235204

236-
if drv or root then
205+
if drv ~= "" or root ~= "" then
237206
if not drv then
238-
for k = #parts, 1, -1 do
239-
local p = parts[k]
240-
p = _path:convert_altsep(p)
241-
drv = _path:split_root(p)
242-
if drv then
207+
for j = #parts, 1, -1 do
208+
local p = parts[j]
209+
p = _flavor:convert_altsep(p)
210+
drv = _flavor:split_root(p)
211+
if drv ~= "" then
243212
break
244213
end
245214
end
246-
247-
break
248215
end
216+
break
249217
end
250218
end
251219

@@ -259,22 +227,30 @@ end
259227

260228
---@class plenary.Path2
261229
---@field path plenary.path2
262-
---@field private _path plenary._Path
230+
---@field private _flavor plenary._Path
231+
---@field private _raw_parts string[]
263232
---@field drv string drive name, eg. 'C:' (only for Windows)
264233
---@field root string root path (excludes drive name)
265-
---@field relparts string[] relative path parts excluding separators
234+
---@field relparts string[] path separator separated relative path parts
266235
---
267236
---@field filename string
268237
---@field cwd string
269238
---@field private _absolute string? lazy eval'ed fully resolved absolute path
270239
local Path = { path = path }
271240

241+
---@param t plenary.Path2
242+
---@param k string
272243
Path.__index = function(t, k)
273244
local raw = rawget(Path, k)
274245
if raw then
275246
return raw
276247
end
277248

249+
if k == "drv" or k == "root" or k == "relparts" then
250+
t.drv, t.root, t.relparts = parse_parts(t._raw_parts, t._flavor)
251+
return rawget(t, k)
252+
end
253+
278254
if k == "filename" then
279255
t.filename = t:_filename()
280256
return t.filename
@@ -337,22 +313,20 @@ function Path:new(...)
337313
end
338314
end
339315

340-
local relparts = {}
316+
local raw_parts = {}
341317
for _, a in ipairs(args) do
342318
if self.is_path(a) then
343-
vim.list_extend(relparts, a.relparts)
319+
vim.list_extend(raw_parts, a._raw_parts)
344320
else
345321
if a ~= "" then
346-
table.insert(relparts, a)
322+
table.insert(raw_parts, a)
347323
end
348324
end
349325
end
350326

351-
local _path = iswin and _WindowsPath or _PosixPath
352-
local drv, root
353-
drv, root, relparts = parse_parts(relparts, _path)
327+
local _flavor = iswin and _WindowsPath or _PosixPath
354328

355-
local proxy = { _path = _path, drv = drv, root = root, relparts = relparts }
329+
local proxy = { _flavor = _flavor, _raw_parts = raw_parts }
356330
setmetatable(proxy, Path)
357331

358332
local obj = { __inner = proxy }
@@ -385,17 +359,17 @@ end
385359
---@return string
386360
function Path:_filename(drv, root, relparts)
387361
drv = vim.F.if_nil(drv, self.drv)
388-
drv = self.drv ~= "" and self.drv:gsub(self._path.sep, path.sep) or ""
362+
drv = self.drv ~= "" and self.drv:gsub(self._flavor.sep, path.sep) or ""
389363

390-
if self._path.has_drv and drv == "" then
364+
if self._flavor.has_drv and drv == "" then
391365
root = ""
392366
else
393367
root = vim.F.if_nil(root, self.root)
394368
root = self.root ~= "" and path.sep:rep(#self.root) or ""
395369
end
396370

397371
relparts = vim.F.if_nil(relparts, self.relparts)
398-
local relpath = table.concat(relparts, path.sep)
372+
local relpath = table.concat(relparts, path.sep)
399373

400374
return drv .. root .. relpath
401375
end
@@ -412,7 +386,7 @@ function Path:is_absolute()
412386
return false
413387
end
414388

415-
return self._path.has_drv and self.drv ~= ""
389+
return self._flavor.has_drv and self.drv ~= ""
416390
end
417391

418392
---@return boolean
@@ -535,8 +509,8 @@ function Path:make_relative(to)
535509
end
536510

537511
-- vim.o.shellslash = false
538-
-- vim.print(p)
539-
-- print(p.filename, p:is_absolute(), p:absolute())
512+
local p = Path:new { "C:", "lua", "..", "README.md" }
513+
print(p.filename)
540514
-- vim.o.shellslash = true
541515

542516
return Path

tests/plenary/path2_spec.lua

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ describe("Path2", function()
4949
describe("filename", function()
5050
local function get_paths()
5151
local readme_path = vim.fn.fnamemodify("README.md", ":p")
52+
local license_path = vim.fn.fnamemodify("LICENSE", ":p")
5253

5354
---@type [string[]|string, string][]
5455
local paths = {
@@ -60,6 +61,7 @@ describe("Path2", function()
6061
{ "./lua//..//README.md", "lua/../README.md" },
6162
{ "foo/bar/", "foo/bar" },
6263
{ { readme_path }, readme_path },
64+
{ { readme_path, license_path }, license_path }, -- takes only the last abs path
6365
}
6466

6567
return paths
@@ -70,7 +72,7 @@ describe("Path2", function()
7072
local input, expect = tc[1], tc[2]
7173
it(vim.inspect(input), function()
7274
local p = Path:new(input)
73-
assert.are.same(expect, p.filename, p.parts)
75+
assert.are.same(expect, p.filename, p.relparts)
7476
end)
7577
end
7678
end
@@ -107,6 +109,8 @@ describe("Path2", function()
107109
{ [[foo/bar\baz]], [[foo/bar/baz]] },
108110
{ [[\\.\C:\Test\Foo.txt]], [[//./C:/Test/Foo.txt]] },
109111
{ [[\\?\C:\Test\Foo.txt]], [[//?/C:/Test/Foo.txt]] },
112+
{ [[\\.\UNC\Server\Share\Test\Foo.txt]], [[//./UNC/Server/Share/Test/Foo.txt]] },
113+
{ [[\\?\UNC\Server\Share\Test\Foo.txt]], [[//?/UNC/Server/Share/Test/Foo.txt]] },
110114
{ "/foo/bar/baz", "foo/bar/baz" },
111115
}
112116
vim.list_extend(paths, get_paths())

0 commit comments

Comments
 (0)