Skip to content

Commit 9992fd8

Browse files
committed
pass most of make_relative
1 parent ecde342 commit 9992fd8

File tree

2 files changed

+175
-91
lines changed

2 files changed

+175
-91
lines changed

lua/plenary/path2.lua

Lines changed: 105 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,8 @@ path.root = (function()
263263
else
264264
return function(base)
265265
base = base or path.home
266-
local _, root, _ = _WindowsPath:split_root(base)
267-
return root
266+
local drv, root, _ = _WindowsPath:split_root(base)
267+
return ((drv .. root):gsub("\\", path.sep))
268268
end
269269
end
270270
end)()
@@ -309,10 +309,10 @@ end
309309
---@field path plenary.path2
310310
---@field private _flavor plenary._Path
311311
---@field private _raw_parts string[]
312-
---@field drv string drive name, eg. 'C:' (only for Windows)
313-
---@field root string root path (excludes drive name)
312+
---@field drv string drive name, eg. 'C:' (only for Windows, empty string for Posix)
313+
---@field root string root path (excludes drive name for Windows)
314314
---@field relparts string[] path separator separated relative path parts
315-
---
315+
---@field sep string path separator (respects 'shellslash' on Windows)
316316
---@field filename string
317317
---@field cwd string
318318
---@field private _absolute string? lazy eval'ed fully resolved absolute path
@@ -336,6 +336,10 @@ Path.__index = function(t, k)
336336
return t.filename
337337
end
338338

339+
if k == "sep" then
340+
return path.sep
341+
end
342+
339343
if k == "cwd" then
340344
t.cwd = vim.fn.getcwd()
341345
return t.cwd
@@ -445,17 +449,17 @@ end
445449
---@return string
446450
function Path:_filename(drv, root, relparts)
447451
drv = vim.F.if_nil(drv, self.drv)
448-
drv = self.drv ~= "" and self.drv:gsub(self._flavor.sep, path.sep) or ""
452+
drv = self.drv ~= "" and self.drv:gsub(self._flavor.sep, self.sep) or ""
449453

450454
if self._flavor.has_drv and drv == "" then
451455
root = ""
452456
else
453457
root = vim.F.if_nil(root, self.root)
454-
root = self.root ~= "" and path.sep:rep(#self.root) or ""
458+
root = self.root ~= "" and self.sep:rep(#self.root) or ""
455459
end
456460

457461
relparts = vim.F.if_nil(relparts, self.relparts)
458-
local relpath = table.concat(relparts, path.sep)
462+
local relpath = table.concat(relparts, self.sep)
459463

460464
return drv .. root .. relpath
461465
end
@@ -538,10 +542,11 @@ function Path:absolute()
538542
else
539543
-- using fs_realpath over fnamemodify
540544
-- fs_realpath resolves symlinks whereas fnamemodify doesn't but we're
541-
-- resolving/normalizing the path anyways for reasons of compat with old Path
545+
-- resolving/normalizing the path anyways for reasons of compat with old
546+
-- Path
542547
local p = uv.fs_realpath(self:_filename()) or Path:new({ self.cwd, self }):absolute()
543548
if self.path.isshellslash then
544-
self._absolute = p:gsub("\\", path.sep)
549+
self._absolute = p:gsub("\\", self.sep)
545550
else
546551
self._absolute = p
547552
end
@@ -555,52 +560,118 @@ function Path:joinpath(...)
555560
return Path:new { self, ... }
556561
end
557562

563+
---@return plenary.Path2
564+
function Path:parent()
565+
local parent = self:iter_parents()()
566+
if parent == nil then
567+
return Path:new(self.filename)
568+
end
569+
return Path:new(parent)
570+
end
571+
572+
--- a list of the path's logical parents.
573+
--- path is made absolute using cwd if relative
558574
---@return string[] # a list of the path's logical parents
559575
function Path:parents()
560576
local res = {}
561-
local abs = self:absolute()
562-
577+
for p in self:iter_parents() do
578+
table.insert(res, p)
579+
end
563580
return res
564581
end
565582

566-
--- makes a path relative to another (by default the cwd).
567-
--- if path is already a relative path
568-
---@param to string|plenary.Path2? absolute path to make relative to (default: cwd)
569-
---@return string
570-
function Path:make_relative(to)
571-
to = vim.F.if_nil(to, self.cwd)
572-
if type(to) == "string" then
583+
---@return fun(): string? # iterator function
584+
function Path:iter_parents()
585+
local abs = Path:new(self:absolute())
586+
local root_part = abs.drv .. abs.root
587+
root_part = self.path.isshellslash and root_part:gsub("\\", self.sep) or root_part
588+
589+
local root_sent = #abs.relparts == 0
590+
return function()
591+
table.remove(abs.relparts)
592+
if #abs.relparts < 1 then
593+
if not root_sent then
594+
root_sent = true
595+
return root_part
596+
end
597+
return nil
598+
end
599+
return root_part .. table.concat(abs.relparts, self.sep)
600+
end
601+
end
602+
603+
--- return true if the path is relative to another, otherwise false
604+
---@param to plenary.Path2|string path to compare to
605+
---@return boolean
606+
function Path:is_relative(to)
607+
if not Path.is_path(to) then
573608
to = Path:new(to)
574609
end
610+
---@cast to plenary.Path2
575611

576-
if self:is_absolute() then
577-
local to_abs = to:absolute()
612+
local to_abs = to:absolute()
613+
return self:absolute():sub(1, #to_abs) == to_abs
614+
end
578615

579-
if to_abs == self:absolute() then
616+
--- makes a path relative to another (by default the cwd).
617+
--- if path is already a relative path, it will first be turned absolute using
618+
--- the cwd then made relative to the `to` path.
619+
---@param to string|plenary.Path2? absolute path to make relative to (default: cwd)
620+
---@param walk_up boolean? walk up to the provided path using '..' (default: `false`)
621+
---@return string
622+
function Path:make_relative(to, walk_up)
623+
walk_up = vim.F.if_nil(walk_up, false)
624+
625+
if to == nil then
626+
if not self:is_absolute() then
580627
return "."
581-
else
582-
-- TODO
583628
end
584-
else
629+
630+
to = Path:new(self.cwd)
631+
elseif type(to) == "string" then
632+
to = Path:new(to) ---@cast to plenary.Path2
585633
end
586634

587-
-- SEE: `Path.relative_to` implementation (3.12) specifically `walk_up` param
635+
local abs = self:absolute()
636+
if abs == to:absolute() then
637+
return "."
638+
end
639+
640+
if self:is_relative(to) then
641+
return Path:new(abs:sub(#to:absolute() + 1)).filename
642+
end
588643

589-
local matches = true
590-
for i = 1, #to.relparts do
591-
if to.relparts[i] ~= self.relparts[i] then
592-
matches = false
644+
if not walk_up then
645+
error(string.format("'%s' is not in the subpath of '%s'", self, to))
646+
end
647+
648+
local steps = {}
649+
local common_path
650+
for parent in to:iter_parents() do
651+
table.insert(steps, "..")
652+
print(parent, abs)
653+
if abs:sub(1, #parent) == parent then
654+
common_path = parent
593655
break
594656
end
595657
end
596658

597-
if matches then
598-
return "."
659+
if not common_path then
660+
error(string.format("'%s' and '%s' have different anchors", self, to))
599661
end
600662

601-
-- /home/jt/foo/bar/baz
602-
-- /home/jt
663+
local res_path = abs:sub(#common_path + 1)
664+
return table.concat(steps, self.sep) .. res_path
603665
end
604666

667+
-- vim.o.shellslash = false
668+
669+
local root = "C:/"
670+
local p = Path:new(Path.path.root(vim.fn.getcwd()))
671+
vim.print("p parent", p.filename, p:parents(), p:parent().filename)
672+
-- local absolute = p:absolute()
673+
-- local relative = Path:new(absolute):make_relative(Path:new "C:/Windows", true)
674+
-- print(p.filename, absolute, relative)
675+
vim.o.shellslash = true
605676

606677
return Path

tests/plenary/path2_spec.lua

Lines changed: 70 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
local Path = require "plenary.path2"
22
local path = Path.path
3-
-- local compat = require "plenary.compat"
3+
local compat = require "plenary.compat"
44
local iswin = vim.loop.os_uname().sysname == "Windows_NT"
55

66
local hasshellslash = vim.fn.exists "+shellslash" == 1
@@ -12,25 +12,21 @@ local function set_shellslash(bool)
1212
end
1313
end
1414

15-
local function it_ssl(name, test_fn)
16-
if not hasshellslash then
17-
it(name, test_fn)
18-
else
19-
local orig = vim.o.shellslash
20-
vim.o.shellslash = true
21-
it(name .. " - shellslash", test_fn)
22-
23-
vim.o.shellslash = false
24-
it(name .. " - noshellslash", test_fn)
25-
vim.o.shellslash = orig
26-
end
27-
end
28-
2915
local function it_cross_plat(name, test_fn)
3016
if not iswin then
3117
it(name .. " - unix", test_fn)
3218
else
33-
it_ssl(name .. " - windows", test_fn)
19+
if not hasshellslash then
20+
it(name .. " - windows", test_fn)
21+
else
22+
local orig = vim.o.shellslash
23+
vim.o.shellslash = true
24+
it(name .. " - windows (shellslash)", test_fn)
25+
26+
vim.o.shellslash = false
27+
it(name .. " - windows (noshellslash)", test_fn)
28+
vim.o.shellslash = orig
29+
end
3430
end
3531
end
3632

@@ -179,7 +175,7 @@ describe("Path2", function()
179175

180176
local function get_windows_paths()
181177
local nossl = hasshellslash and not vim.o.shellslash
182-
local drive = Path:new(vim.loop.cwd()).drv
178+
local drive = Path:new(vim.fn.getcwd()).drv
183179
local readme_path = vim.fn.fnamemodify("README.md", ":p")
184180

185181
---@type [string[]|string, string, boolean][]
@@ -273,56 +269,56 @@ describe("Path2", function()
273269

274270
describe(":make_relative", function()
275271
local root = iswin and "c:\\" or "/"
276-
-- it_cross_plat("can take absolute paths and make them relative to the cwd", function()
277-
-- local p = Path:new { "lua", "plenary", "path.lua" }
278-
-- local absolute = vim.loop.cwd() .. path.sep .. p.filename
279-
-- local relative = Path:new(absolute):make_relative()
280-
-- assert.are.same(p.filename, relative)
281-
-- end)
272+
it_cross_plat("can take absolute paths and make them relative to the cwd", function()
273+
local p = Path:new { "lua", "plenary", "path.lua" }
274+
local absolute = vim.fn.getcwd() .. path.sep .. p.filename
275+
local relative = Path:new(absolute):make_relative()
276+
assert.are.same(p.filename, relative)
277+
end)
282278

283-
-- it_cross_plat("can take absolute paths and make them relative to a given path", function()
284-
-- local r = Path:new { root, "home", "prime" }
285-
-- local p = Path:new { "aoeu", "agen.lua" }
286-
-- local absolute = r.filename .. path.sep .. p.filename
287-
-- local relative = Path:new(absolute):make_relative(r.filename)
288-
-- assert.are.same(relative, p.filename)
289-
-- end)
279+
it_cross_plat("can take absolute paths and make them relative to a given path", function()
280+
local r = Path:new { root, "home", "prime" }
281+
local p = Path:new { "aoeu", "agen.lua" }
282+
local absolute = r.filename .. path.sep .. p.filename
283+
local relative = Path:new(absolute):make_relative(r.filename)
284+
assert.are.same(p.filename, relative)
285+
end)
290286

291-
-- it_cross_plat("can take double separator absolute paths and make them relative to the cwd", function()
292-
-- local p = Path:new { "lua", "plenary", "path.lua" }
293-
-- local absolute = vim.loop.cwd() .. path.sep .. path.sep .. p.filename
294-
-- local relative = Path:new(absolute):make_relative()
295-
-- assert.are.same(relative, p.filename)
296-
-- end)
287+
it_cross_plat("can take double separator absolute paths and make them relative to the cwd", function()
288+
local p = Path:new { "lua", "plenary", "path.lua" }
289+
local absolute = vim.fn.getcwd() .. path.sep .. path.sep .. p.filename
290+
local relative = Path:new(absolute):make_relative()
291+
assert.are.same(p.filename, relative)
292+
end)
297293

298-
-- it_cross_plat("can take double separator absolute paths and make them relative to a given path", function()
299-
-- local r = Path:new { root, "home", "prime" }
300-
-- local p = Path:new { "aoeu", "agen.lua" }
301-
-- local absolute = r.filename .. path.sep .. path.sep .. p.filename
302-
-- local relative = Path:new(absolute):make_relative(r.filename)
303-
-- assert.are.same(relative, p.filename)
304-
-- end)
294+
it_cross_plat("can take double separator absolute paths and make them relative to a given path", function()
295+
local r = Path:new { root, "home", "prime" }
296+
local p = Path:new { "aoeu", "agen.lua" }
297+
local absolute = r.filename .. path.sep .. path.sep .. p.filename
298+
local relative = Path:new(absolute):make_relative(r.filename)
299+
assert.are.same(p.filename, relative)
300+
end)
305301

306-
-- it_cross_plat("can take absolute paths and make them relative to a given path with trailing separator", function()
307-
-- local r = Path:new { root, "home", "prime" }
308-
-- local p = Path:new { "aoeu", "agen.lua" }
309-
-- local absolute = r.filename .. path.sep .. p.filename
310-
-- local relative = Path:new(absolute):make_relative(r.filename .. path.sep)
311-
-- assert.are.same(relative, p.filename)
312-
-- end)
302+
it_cross_plat("can take absolute paths and make them relative to a given path with trailing separator", function()
303+
local r = Path:new { root, "home", "prime" }
304+
local p = Path:new { "aoeu", "agen.lua" }
305+
local absolute = r.filename .. path.sep .. p.filename
306+
local relative = Path:new(absolute):make_relative(r.filename .. path.sep)
307+
assert.are.same(p.filename, relative)
308+
end)
313309

314310
-- it_cross_plat("can take absolute paths and make them relative to the root directory", function()
315-
-- local p = Path:new { "home", "prime", "aoeu", "agen.lua" }
311+
-- local p = Path:new { root, "prime", "aoeu", "agen.lua" }
316312
-- local absolute = root .. p.filename
317313
-- local relative = Path:new(absolute):make_relative(root)
318-
-- assert.are.same(relative, p.filename)
314+
-- assert.are.same(p.filename, relative)
319315
-- end)
320316

321-
-- it_cross_plat("can take absolute paths and make them relative to themselves", function()
322-
-- local p = Path:new { root, "home", "prime", "aoeu", "agen.lua" }
323-
-- local relative = Path:new(p.filename):make_relative(p.filename)
324-
-- assert.are.same(relative, ".")
325-
-- end)
317+
it_cross_plat("can take absolute paths and make them relative to themselves", function()
318+
local p = Path:new { root, "home", "prime", "aoeu", "agen.lua" }
319+
local relative = Path:new(p.filename):make_relative(p.filename)
320+
assert.are.same(".", relative)
321+
end)
326322

327323
-- it_cross_plat("should not truncate if path separator is not present after cwd", function()
328324
-- local cwd = "tmp" .. path.sep .. "foo"
@@ -338,4 +334,21 @@ describe("Path2", function()
338334
-- assert.are.same(p.filename, relative)
339335
-- end)
340336
end)
337+
338+
describe("parents", function()
339+
it_cross_plat("should extract the ancestors of the path", function()
340+
local p = Path:new(vim.fn.getcwd())
341+
local parents = p:parents()
342+
assert(compat.islist(parents))
343+
for _, parent in pairs(parents) do
344+
assert.are.same(type(parent), "string")
345+
end
346+
end)
347+
348+
it_cross_plat("should return itself if it corresponds to path.root", function()
349+
local p = Path:new(Path.path.root(vim.fn.getcwd()))
350+
assert.are.same(p:absolute(), p:parent():absolute())
351+
-- assert.are.same(p, p:parent())
352+
end)
353+
end)
341354
end)

0 commit comments

Comments
 (0)