@@ -31,6 +31,7 @@ local hasshellslash = vim.fn.exists "+shellslash" == 1
31
31
--- @field case_sensitive boolean
32
32
--- @field convert_altsep fun ( self : plenary._Path , p : string ): string
33
33
--- @field split_root fun ( self : plenary._Path , part : string ): string , string , string
34
+ --- @field join fun ( self : plenary._Path , path : string , ... : string ): string
34
35
35
36
--- @class plenary._WindowsPath : plenary._Path
36
37
local _WindowsPath = {
@@ -48,54 +49,115 @@ function _WindowsPath:convert_altsep(p)
48
49
return (p :gsub (self .altsep , self .sep ))
49
50
end
50
51
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
52
59
--- @return string drv
53
60
--- @return string root
54
61
--- @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
+
57
65
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 )
59
67
60
68
if first == self .sep then
61
69
if second == self .sep then
62
70
-- UNC drives, e.g. \\server\share or \\?\UNC\server\share
63
71
-- 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 )
66
74
if index == nil then
67
- return part , " " , " " -- paths only has drive info
75
+ return p , " " , " " -- paths only has drive info
68
76
end
69
77
70
- local index2 = part :find (self .sep , index + 1 )
78
+ local index2 = p :find (self .sep , index + 1 )
71
79
if index2 == nil then
72
- return part , " " , " " -- still paths only has drive info
80
+ return p , " " , " " -- still paths only has drive info
73
81
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 )
75
83
else
76
84
-- Relative path with root, eg. \Windows
77
- return " " , part :sub (1 , 1 ), part :sub (2 )
85
+ return " " , p :sub (1 , 1 ), p :sub (2 )
78
86
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
81
89
-- 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 )
83
91
else
84
92
-- 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 )
86
94
end
87
95
else
88
96
-- relative path, eg. Windows
89
- return " " , " " , part
97
+ return " " , " " , p
90
98
end
91
99
end
92
100
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
+
93
155
--- @class plenary._PosixPath : plenary._Path
94
156
local _PosixPath = {
95
157
sep = " /" ,
96
158
altsep = " " ,
97
159
has_drv = false ,
98
- case_sensitive = true ,
160
+ case_sensitive = false ,
99
161
}
100
162
setmetatable (_PosixPath , { __index = _PosixPath })
101
163
@@ -116,6 +178,40 @@ function _PosixPath:split_root(part)
116
178
return " " , " " , part
117
179
end
118
180
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
+
119
215
local S_IF = {
120
216
-- S_IFDIR = 0o040000 # directory
121
217
DIR = 0x4000 ,
@@ -181,44 +277,29 @@ end)()
181
277
local function parse_parts (parts , _flavor )
182
278
local drv , root , rel , parsed = " " , " " , " " , {}
183
279
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
203
283
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
216
296
end
217
297
end
218
298
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
222
303
end
223
304
224
305
return drv , root , parsed
@@ -282,14 +363,37 @@ end
282
363
--- @return boolean
283
364
Path .__eq = function (self , other )
284
365
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 ()
291
376
end
292
377
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
+
293
397
--- @alias plenary.Path2Args string | plenary.Path2 | (string | plenary.Path2 )[]
294
398
295
399
--- @param ... plenary .Path2Args
@@ -329,24 +433,7 @@ function Path:new(...)
329
433
setmetatable (proxy , Path )
330
434
331
435
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 )
350
437
351
438
return obj
352
439
end
@@ -468,6 +555,14 @@ function Path:joinpath(...)
468
555
return Path :new { self , ... }
469
556
end
470
557
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
+
471
566
--- makes a path relative to another (by default the cwd).
472
567
--- if path is already a relative path
473
568
--- @param to string | plenary.Path2 ? absolute path to make relative to (default : cwd )
@@ -507,10 +602,5 @@ function Path:make_relative(to)
507
602
-- /home/jt
508
603
end
509
604
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
515
605
516
606
return Path
0 commit comments