Skip to content

Commit ecde342

Browse files
committed
more instantiation improvements
1 parent 8c903b7 commit ecde342

File tree

2 files changed

+237
-147
lines changed

2 files changed

+237
-147
lines changed

lua/plenary/path2.lua

Lines changed: 170 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ local hasshellslash = vim.fn.exists "+shellslash" == 1
3131
---@field case_sensitive boolean
3232
---@field convert_altsep fun(self: plenary._Path, p:string): string
3333
---@field split_root fun(self: plenary._Path, part:string): string, string, string
34+
---@field join fun(self: plenary._Path, path: string, ...: string): string
3435

3536
---@class plenary._WindowsPath : plenary._Path
3637
local _WindowsPath = {
@@ -48,54 +49,115 @@ function _WindowsPath:convert_altsep(p)
4849
return (p:gsub(self.altsep, self.sep))
4950
end
5051

51-
---@param part string path with only `\` separators
52+
--- splits path into drive, root, and relative path components
53+
--- split_root('//server/share/') == { '//server/share', '/', '' }
54+
--- split_root('C:/Users/Barney') == { 'C:', '/', 'Users/Barney' }
55+
--- split_root('C:///spam///ham') == { 'C:', '/', '//spam///ham' }
56+
--- split_root('Windows/notepad') == { '', '', 'Windows/notepad' }
57+
--- https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
58+
---@param p string path with only `\` separators
5259
---@return string drv
5360
---@return string root
5461
---@return string relpath
55-
function _WindowsPath:split_root(part)
56-
-- https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
62+
function _WindowsPath:split_root(p)
63+
p = self:convert_altsep(p)
64+
5765
local unc_prefix = "\\\\?\\UNC\\"
58-
local first, second = part:sub(1, 1), part:sub(2, 2)
66+
local first, second = p:sub(1, 1), p:sub(2, 2)
5967

6068
if first == self.sep then
6169
if second == self.sep then
6270
-- UNC drives, e.g. \\server\share or \\?\UNC\server\share
6371
-- 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)
72+
local start = p:sub(1, 8):upper() == unc_prefix and 8 or 2
73+
local index = p:find(self.sep, start)
6674
if index == nil then
67-
return part, "", "" -- paths only has drive info
75+
return p, "", "" -- paths only has drive info
6876
end
6977

70-
local index2 = part:find(self.sep, index + 1)
78+
local index2 = p:find(self.sep, index + 1)
7179
if index2 == nil then
72-
return part, "", "" -- still paths only has drive info
80+
return p, "", "" -- still paths only has drive info
7381
end
74-
return part:sub(1, index2 - 1), self.sep, part:sub(index2 + 1)
82+
return p:sub(1, index2 - 1), self.sep, p:sub(index2 + 1)
7583
else
7684
-- Relative path with root, eg. \Windows
77-
return "", part:sub(1, 1), part:sub(2)
85+
return "", p:sub(1, 1), p:sub(2)
7886
end
79-
elseif part:sub(2, 2) == ":" then
80-
if part:sub(3, 3) == self.sep then
87+
elseif p:sub(2, 2) == ":" then
88+
if p:sub(3, 3) == self.sep then
8189
-- absolute path with drive, eg. C:\Windows
82-
return part:sub(1, 2), self.sep, part:sub(3)
90+
return p:sub(1, 2), self.sep, p:sub(3)
8391
else
8492
-- relative path with drive, eg. C:Windows
85-
return part:sub(1, 2), "", part:sub(3)
93+
return p:sub(1, 2), "", p:sub(3)
8694
end
8795
else
8896
-- relative path, eg. Windows
89-
return "", "", part
97+
return "", "", p
9098
end
9199
end
92100

101+
---@param path string
102+
---@param ... string
103+
---@return string
104+
function _WindowsPath:join(path, ...)
105+
local paths = { ... }
106+
107+
local result_drive, result_root, result_path = self:split_root(path)
108+
local parts = {}
109+
110+
if result_path ~= "" then
111+
table.insert(parts, result_path)
112+
end
113+
114+
for _, p in ipairs(paths) do
115+
p = self:convert_altsep(p)
116+
local p_drive, p_root, p_path = self:split_root(p)
117+
118+
if p_root ~= "" then
119+
-- second path is absolute
120+
if p_drive ~= "" or result_drive == "" then
121+
result_drive = p_drive
122+
end
123+
result_root = p_root
124+
parts = { p_path }
125+
elseif p_drive ~= "" and p_drive:lower() ~= result_drive:lower() then
126+
-- drive letter is case insensitive
127+
-- here they don't match => ignore first path, later paths take precedence
128+
result_drive, result_root, parts = p_drive, p_root, { p_path }
129+
else
130+
if p_drive ~= "" then
131+
result_drive = p_drive
132+
end
133+
134+
if #parts > 0 and parts[#parts]:sub(-1) ~= self.sep then
135+
table.insert(parts, self.sep)
136+
end
137+
138+
table.insert(parts, p_path)
139+
end
140+
end
141+
142+
local drv_last_ch = result_drive:sub(-1)
143+
if
144+
result_path ~= ""
145+
and result_root == ""
146+
and result_drive ~= ""
147+
and not (drv_last_ch == self.sep or drv_last_ch == ":")
148+
then
149+
return result_drive .. self.sep .. table.concat(parts)
150+
end
151+
152+
return result_drive .. result_root .. table.concat(parts)
153+
end
154+
93155
---@class plenary._PosixPath : plenary._Path
94156
local _PosixPath = {
95157
sep = "/",
96158
altsep = "",
97159
has_drv = false,
98-
case_sensitive = true,
160+
case_sensitive = false,
99161
}
100162
setmetatable(_PosixPath, { __index = _PosixPath })
101163

@@ -116,6 +178,40 @@ function _PosixPath:split_root(part)
116178
return "", "", part
117179
end
118180

181+
---@param path string
182+
---@param ... string
183+
---@return string
184+
function _PosixPath:join(path, ...)
185+
local paths = { ... }
186+
local parts = {}
187+
188+
if path ~= "" then
189+
table.insert(parts, path)
190+
end
191+
192+
for _, p in ipairs(paths) do
193+
if p:sub(1, 1) == self.sep then
194+
parts = { p } -- is absolute, ignore previous path, later paths take precedence
195+
elseif path == "" or path:sub(-1) == self.sep then
196+
table.insert(parts, p)
197+
else
198+
table.insert(parts, self.sep .. p)
199+
end
200+
end
201+
return table.concat(parts)
202+
end
203+
204+
--[[
205+
206+
for b in map(os.fspath, p):
207+
if b.startswith(sep):
208+
path = b
209+
elif not path or path.endswith(sep):
210+
path += b
211+
else:
212+
path += sep + b
213+
]]
214+
119215
local S_IF = {
120216
-- S_IFDIR = 0o040000 # directory
121217
DIR = 0x4000,
@@ -181,44 +277,29 @@ end)()
181277
local function parse_parts(parts, _flavor)
182278
local drv, root, rel, parsed = "", "", "", {}
183279

184-
for i = #parts, 1, -1 do
185-
local part = parts[i]
186-
part = _flavor:convert_altsep(part)
187-
188-
drv, root, rel = _flavor:split_root(part)
189-
190-
if rel:match(_flavor.sep) then
191-
local relparts = vim.split(rel, _flavor.sep)
192-
for j = #relparts, 1, -1 do
193-
local p = relparts[j]
194-
if p ~= "" and p ~= "." then
195-
table.insert(parsed, p)
196-
end
197-
end
198-
else
199-
if rel ~= "" and rel ~= "." then
200-
table.insert(parsed, rel)
201-
end
202-
end
280+
if #parts == 0 then
281+
return drv, root, parsed
282+
end
203283

204-
if drv ~= "" or root ~= "" then
205-
if not drv then
206-
for j = #parts, 1, -1 do
207-
local p = parts[j]
208-
p = _flavor:convert_altsep(p)
209-
drv = _flavor:split_root(p)
210-
if drv ~= "" then
211-
break
212-
end
213-
end
214-
end
215-
break
284+
local sep = _flavor.sep
285+
local p = _flavor:join(unpack(parts))
286+
drv, root, rel = _flavor:split_root(p)
287+
288+
if root == "" and drv:sub(1, 1) == sep and drv:sub(-1) ~= sep then
289+
local drv_parts = vim.split(drv, sep)
290+
if #drv_parts == 4 and not (drv_parts[3] == "?" or drv_parts[3] == ".") then
291+
-- e.g. //server/share
292+
root = sep
293+
elseif #drv_parts == 6 then
294+
-- e.g. //?/unc/server/share
295+
root = sep
216296
end
217297
end
218298

219-
local n = #parsed
220-
for i = 1, math.floor(n / 2) do
221-
parsed[i], parsed[n - i + 1] = parsed[n - i + 1], parsed[i]
299+
for part in vim.gsplit(rel, sep) do
300+
if part ~= "" and part ~= "." then
301+
table.insert(parsed, part)
302+
end
222303
end
223304

224305
return drv, root, parsed
@@ -282,14 +363,37 @@ end
282363
---@return boolean
283364
Path.__eq = function(self, other)
284365
assert(Path.is_path(self))
285-
assert(Path.is_path(other) or type(other) == "string")
286-
-- TODO
287-
-- if true then
288-
-- error "not yet implemented"
289-
-- end
290-
return self.filename == other.filename
366+
367+
local oth_type_str = type(other) == "string"
368+
assert(Path.is_path(other) or oth_type_str)
369+
370+
if oth_type_str then
371+
other = Path:new(other)
372+
end
373+
---@cast other plenary.Path2
374+
375+
return self:absolute() == other:absolute()
291376
end
292377

378+
local _readonly_mt = {
379+
__index = function(t, k)
380+
return t.__inner[k]
381+
end,
382+
__newindex = function(t, k, val)
383+
if k == "_absolute" then
384+
t.__inner[k] = val
385+
return
386+
end
387+
error "'Path' object is read-only"
388+
end,
389+
-- stylua: ignore start
390+
__div = function(t, other) return Path.__div(t, other) end,
391+
__tostring = function(t) return Path.__tostring(t) end,
392+
__eq = function(t, other) return Path.__eq(t, other) end, -- this never gets called
393+
__metatable = Path,
394+
-- stylua: ignore end
395+
}
396+
293397
---@alias plenary.Path2Args string|plenary.Path2|(string|plenary.Path2)[]
294398

295399
---@param ... plenary.Path2Args
@@ -329,24 +433,7 @@ function Path:new(...)
329433
setmetatable(proxy, Path)
330434

331435
local obj = { __inner = proxy }
332-
setmetatable(obj, {
333-
__index = function(_, k)
334-
return proxy[k]
335-
end,
336-
__newindex = function(_, k, val)
337-
if k == "_absolute" then
338-
proxy[k] = val
339-
return
340-
end
341-
error "'Path' object is read-only"
342-
end,
343-
-- stylua: ignore start
344-
__div = function(t, other) return Path.__div(t, other) end,
345-
__tostring = function(t) return Path.__tostring(t) end,
346-
__eq = function(t, other) return Path.__eq(t, other) end,
347-
__metatable = Path,
348-
-- stylua: ignore end
349-
})
436+
setmetatable(obj, _readonly_mt)
350437

351438
return obj
352439
end
@@ -468,6 +555,14 @@ function Path:joinpath(...)
468555
return Path:new { self, ... }
469556
end
470557

558+
---@return string[] # a list of the path's logical parents
559+
function Path:parents()
560+
local res = {}
561+
local abs = self:absolute()
562+
563+
return res
564+
end
565+
471566
--- makes a path relative to another (by default the cwd).
472567
--- if path is already a relative path
473568
---@param to string|plenary.Path2? absolute path to make relative to (default: cwd)
@@ -507,10 +602,5 @@ function Path:make_relative(to)
507602
-- /home/jt
508603
end
509604

510-
-- vim.o.shellslash = false
511-
local p = Path:new { "/mnt/c/Users/jtrew/neovim/plenary.nvim/README.md" }
512-
vim.print(p.drv, p.root, p.relparts)
513-
print(p.filename, p:is_absolute())
514-
-- vim.o.shellslash = true
515605

516606
return Path

0 commit comments

Comments
 (0)