Skip to content

Commit 31b7ab2

Browse files
committed
add normalize and clean up
1 parent 8c78935 commit 31b7ab2

File tree

2 files changed

+143
-77
lines changed

2 files changed

+143
-77
lines changed

lua/plenary/path2.lua

Lines changed: 79 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ local hasshellslash = vim.fn.exists "+shellslash" == 1
1313
---@field split_root fun(self: plenary._Path, part:string): string, string, string
1414
---@field join fun(self: plenary._Path, path: string, ...: string): string
1515
---@field expand fun(self: plenary._Path, parts: string[], sep: string?): string[]
16+
---@field is_relative fun(self: plenary._Path, path: plenary.Path2, to: plenary.Path2): boolean
1617

1718
---@class plenary._WindowsPath : plenary._Path
1819
local _WindowsPath = {
1920
sep = "\\",
2021
altsep = "/",
2122
has_drv = true,
22-
case_sensitive = true,
23+
case_sensitive = false,
2324
}
2425

2526
setmetatable(_WindowsPath, { __index = _WindowsPath })
@@ -165,12 +166,28 @@ function _WindowsPath:expand(parts, sep)
165166
return new_parts
166167
end
167168

169+
---@param path plenary.Path2
170+
---@param to plenary.Path2
171+
---@return boolean
172+
function _WindowsPath:is_relative(path, to)
173+
if path.anchor:lower() ~= to.anchor:lower() then
174+
return false
175+
end
176+
177+
for i, to_part in ipairs(to.relparts) do
178+
if to_part:lower() ~= path.relparts[i]:lower() then
179+
return false
180+
end
181+
end
182+
return true
183+
end
184+
168185
---@class plenary._PosixPath : plenary._Path
169186
local _PosixPath = {
170187
sep = "/",
171188
altsep = "",
172189
has_drv = false,
173-
case_sensitive = false,
190+
case_sensitive = true,
174191
}
175192
setmetatable(_PosixPath, { __index = _PosixPath })
176193

@@ -242,6 +259,22 @@ function _PosixPath:expand(parts)
242259
return new_parts
243260
end
244261

262+
---@param path plenary.Path2
263+
---@param to plenary.Path2
264+
---@return boolean
265+
function _PosixPath:is_relative(path, to)
266+
if path.root ~= to.root then
267+
return false
268+
end
269+
270+
for i, to_part in ipairs(to.relparts) do
271+
if to_part ~= path.relparts[i] then
272+
return false
273+
end
274+
end
275+
return true
276+
end
277+
245278
local S_IF = {
246279
-- S_IFDIR = 0o040000 # directory
247280
DIR = 0x4000,
@@ -437,7 +470,14 @@ Path.__eq = function(self, other)
437470
end
438471
---@cast other plenary.Path2
439472

440-
return self:absolute() == other:absolute()
473+
if self._flavor ~= other._flavor then
474+
return false
475+
end
476+
477+
if self._flavor.case_sensitive then
478+
return self.filename == other.filename
479+
end
480+
return self.filename:lower() == other.filename:lower()
441481
end
442482

443483
local _readonly_mt = {
@@ -544,10 +584,7 @@ local function is_path_like(x)
544584
end
545585

546586
local function is_path_like_opt(x)
547-
if x == nil then
548-
return true
549-
end
550-
return is_path_like(x)
587+
return x == nil or is_path_like(x)
551588
end
552589

553590
---@return boolean
@@ -749,41 +786,21 @@ function Path:is_relative(to)
749786
return true
750787
end
751788

752-
-- NOTE: could probably be optimized by letting _WindowsPath/_WindowsPath
753-
-- handle this.
754-
755-
local to_abs = to:absolute()
756-
for parent in self:iter_parents() do
757-
if to_abs == parent then
758-
return true
759-
end
760-
end
761-
762-
return false
789+
return self._flavor:is_relative(self, to)
763790
end
764791

765-
--- makes a path relative to another (by default the cwd).
766-
--- if path is already a relative path, it will first be turned absolute using
767-
--- the cwd then made relative to the `to` path.
768-
---@param to string|plenary.Path2? absolute path to make relative to (default: cwd)
769-
---@param walk_up boolean? walk up to the provided path using '..' (default: `false`)
770-
---@return string
771-
function Path:make_relative(to, walk_up)
792+
---@return plenary.Path2
793+
function Path:_make_relative(to, walk_up)
772794
vim.validate {
773795
to = { to, is_path_like_opt },
774796
walk_up = { walk_up, "b", true },
775797
}
776798

777-
-- NOTE: could probably take some shortcuts and avoid some `Path:new` calls
778-
-- by allowing _WindowsPath/_PosixPath handle this individually.
779-
-- As always, Windows root complicates things, so generating a new Path often
780-
-- easier/less error prone than manual string manipulate but at the cost of
781-
-- perf.
782799
walk_up = vim.F.if_nil(walk_up, false)
783800

784801
if to == nil then
785802
if not self:is_absolute() then
786-
return "."
803+
return Path:new "."
787804
end
788805

789806
to = Path:new(self.cwd)
@@ -793,11 +810,11 @@ function Path:make_relative(to, walk_up)
793810

794811
local abs = self:absolute()
795812
if abs == to:absolute() then
796-
return "."
813+
return Path:new "."
797814
end
798815

799816
if self:is_relative(to) then
800-
return Path:new((abs:sub(#to:absolute() + 1):gsub("^" .. self.sep, ""))).filename
817+
return Path:new((abs:sub(#to:absolute() + 1):gsub("^" .. self.sep, "")))
801818
end
802819

803820
if not walk_up then
@@ -820,7 +837,35 @@ function Path:make_relative(to, walk_up)
820837

821838
local res_path = abs:sub(#common_path + 1):gsub("^" .. self.sep, "")
822839
table.insert(steps, res_path)
823-
return Path:new(steps).filename
840+
return Path:new(steps)
841+
end
842+
843+
--- makes a path relative to another (by default the cwd).
844+
--- if path is already a relative path, it will first be turned absolute using
845+
--- the cwd then made relative to the `to` path.
846+
---@param to string|plenary.Path2? path to make relative to (default: cwd)
847+
---@param walk_up boolean? walk up to the provided path using '..' (default: `false`)
848+
---@return string
849+
function Path:make_relative(to, walk_up)
850+
return self:_make_relative(to, walk_up).filename
851+
end
852+
853+
--- Normalize path, resolving any intermediate ".."
854+
--- eg. `a//b`, `a/./b`, `a/foo/../b` will all become `a/b`
855+
--- Can optionally convert a path to relative to another.
856+
---@param relative_to string|plenary.Path2|nil path to make relative to, if nil path isn't made relative
857+
---@param walk_up boolean? walk up to the make relative path (if provided) using '..' (default: `false`)
858+
---@return string
859+
function Path:normalize(relative_to, walk_up)
860+
local p
861+
if relative_to == nil then
862+
p = self
863+
else
864+
p = self:_make_relative(relative_to, walk_up)
865+
end
866+
867+
local relparts = resolve_dots(p.relparts)
868+
return p:_filename(nil, nil, relparts)
824869
end
825870

826871
--- Shorten path parts.

tests/plenary/path2_spec.lua

Lines changed: 64 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ local function plat_path(p)
4646
return p:gsub("/", "\\")
4747
end
4848

49+
local function root()
50+
if not iswin then
51+
return "/"
52+
end
53+
if hasshellslash and vim.o.shellslash then
54+
return "C:/"
55+
end
56+
return "C:\\"
57+
end
58+
4959
-- set up mock file with consistent eol regardless of system (git autocrlf settings)
5060
-- simplifies reading tests
5161
local licence_lines = {
@@ -301,16 +311,6 @@ describe("Path2", function()
301311
end)
302312

303313
describe(":make_relative", function()
304-
local root = function()
305-
if not iswin then
306-
return "/"
307-
end
308-
if hasshellslash and vim.o.shellslash then
309-
return "C:/"
310-
end
311-
return "C:\\"
312-
end
313-
314314
it_cross_plat("can take absolute paths and make them relative to the cwd", function()
315315
local p = Path:new { "lua", "plenary", "path.lua" }
316316
local absolute = vim.fn.getcwd() .. path.sep .. p.filename
@@ -380,6 +380,57 @@ describe("Path2", function()
380380
local expect = Path:new { "..", "foo", "bar", "baz" }
381381
assert.are.same(expect.filename, p:make_relative(cwd, true))
382382
end)
383+
384+
it_win("handles drive letters case insensitively", function()
385+
local p = Path:new { "C:/", "foo", "bar", "baz" }
386+
local cwd = Path:new { "c:/", "foo" }
387+
local expect = Path:new { "bar", "baz" }
388+
assert.are.same(expect.filename, p:make_relative(cwd))
389+
end)
390+
end)
391+
392+
describe("normalize", function()
393+
it_cross_plat("handles empty path", function()
394+
local p = Path:new ""
395+
assert.are.same(".", p:normalize())
396+
end)
397+
398+
it_cross_plat("removes middle ..", function()
399+
local p = Path:new "lua/../lua/say.lua"
400+
local expect = Path:new { "lua", "say.lua" }
401+
assert.are.same(expect.filename, p:normalize())
402+
end)
403+
404+
it_cross_plat("walk up relative path", function()
405+
local p = Path:new "async/../../lua/say.lua"
406+
local expect = Path:new { "..", "lua", "say.lua" }
407+
assert.are.same(expect.filename, p:normalize())
408+
end)
409+
410+
it_cross_plat("handles absolute path", function()
411+
local p = Path:new { root(), "a", "..", "a", "b" }
412+
local expect = Path:new { root(), "a", "b" }
413+
assert.are.same(expect.filename, p:normalize())
414+
end)
415+
416+
it_cross_plat("makes relative", function()
417+
local p = Path:new { path.home, "a", "..", "", "a", "b" }
418+
local expect = Path:new { "a", "b" }
419+
assert.are.same(expect.filename, p:normalize(path.home))
420+
end)
421+
422+
it_cross_plat("make relative walk_up", function()
423+
local p = Path:new { path.home, "a", "..", "", "a", "b" }
424+
local cwd = Path:new { path.home, "c" }
425+
local expect = Path:new { "..", "a", "b" }
426+
assert.are.same(expect.filename, p:normalize(cwd, true))
427+
end)
428+
429+
it_win("windows drive relative paths", function()
430+
local p = Path:new { "C:", "a", "..", "", "a", "b" }
431+
local expect = Path:new { "C:", "a", "b" }
432+
assert.are.same(expect.filename, p:normalize())
433+
end)
383434
end)
384435

385436
describe(":shorten", function()
@@ -441,13 +492,6 @@ describe("Path2", function()
441492
end)
442493
end)
443494

444-
local function assert_permission(expect, actual)
445-
if iswin then
446-
return
447-
end
448-
assert.equal(expect, actual)
449-
end
450-
451495
describe("mkdir / rmdir", function()
452496
after_each(function()
453497
uv.fs_rmdir "_dir_not_exist"
@@ -464,7 +508,6 @@ describe("Path2", function()
464508
p:mkdir()
465509
assert.is_true(p:exists())
466510
assert.is_true(p:is_dir())
467-
assert_permission(755, p:permission()) -- umask dependent, probably bad test
468511

469512
p:rmdir()
470513
assert.is_false(p:exists())
@@ -497,17 +540,6 @@ describe("Path2", function()
497540
assert.is_false(p:exists())
498541
assert.is_false(Path:new("impossible"):exists())
499542
end)
500-
501-
it_cross_plat("can set different modes", function()
502-
local p = Path:new "_dir_not_exist"
503-
assert.has_no_error(function()
504-
p:mkdir { mode = 292 } -- o444
505-
end)
506-
assert_permission(444, p:permission())
507-
508-
p:rmdir()
509-
assert.is_false(p:exists())
510-
end)
511543
end)
512544

513545
describe("touch/rm", function()
@@ -764,17 +796,6 @@ describe("Path2", function()
764796
table.insert(trg_dirs, trg_dir:joinpath(dir))
765797
end
766798

767-
-- vim.tbl_flatten doesn't work here as copy doesn't return a list
768-
local function flatten(ret, t)
769-
for _, v in pairs(t) do
770-
if type(v) == "table" then
771-
flatten(ret, v)
772-
else
773-
table.insert(ret, v)
774-
end
775-
end
776-
end
777-
778799
before_each(function()
779800
-- generate {file}_{level}.lua on every directory level in src
780801
-- src
@@ -1198,14 +1219,14 @@ SOFTWARE.]]
11981219
local p = Path:new "lua/plenary"
11991220
local res = assert(p:find_upwards "busted.lua")
12001221
local expect = Path:new "lua/plenary/busted.lua"
1201-
assert.are.same(expect, res)
1222+
assert.are.same(expect.filename, res.filename)
12021223
end)
12031224

12041225
it_cross_plat("finds file in parent dir", function()
12051226
local p = Path:new "lua/plenary"
12061227
local res = assert(p:find_upwards "say.lua")
1207-
local expect = Path:new "lua/say.lua"
1208-
assert.are.same(expect, res)
1228+
local expect = vim.fn.fnamemodify("lua/say.lua", ":p")
1229+
assert.are.same(expect, res.filename)
12091230
end)
12101231

12111232
it_cross_plat("doesn't find file", function()

0 commit comments

Comments
 (0)