1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
--- TODO: This is implemented only for files now.
-- https://tools.ietf.org/html/rfc3986
-- https://tools.ietf.org/html/rfc2732
-- https://tools.ietf.org/html/rfc2396
local uri_decode
do
local schar = string.char
--- Convert hex to char
---@private
local function hex_to_char(hex)
return schar(tonumber(hex, 16))
end
uri_decode = function(str)
return str:gsub('%%([a-fA-F0-9][a-fA-F0-9])', hex_to_char)
end
end
local uri_encode
do
local PATTERNS = {
--- RFC 2396
-- https://tools.ietf.org/html/rfc2396#section-2.2
rfc2396 = "^A-Za-z0-9%-_.!~*'()",
--- RFC 2732
-- https://tools.ietf.org/html/rfc2732
rfc2732 = "^A-Za-z0-9%-_.!~*'()[]",
--- RFC 3986
-- https://tools.ietf.org/html/rfc3986#section-2.2
rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/",
}
local sbyte, tohex = string.byte
if jit then
tohex = require('bit').tohex
else
tohex = function(b)
return string.format('%02x', b)
end
end
---@private
local function percent_encode_char(char)
return '%' .. tohex(sbyte(char), 2)
end
uri_encode = function(text, rfc)
if not text then
return
end
local pattern = PATTERNS[rfc] or PATTERNS.rfc3986
return text:gsub('([' .. pattern .. '])', percent_encode_char)
end
end
---@private
local function is_windows_file_uri(uri)
return uri:match('^file:/+[a-zA-Z]:') ~= nil
end
--- Get a URI from a file path.
---@param path string Path to file
---@return string URI
local function uri_from_fname(path)
local volume_path, fname = path:match('^([a-zA-Z]:)(.*)')
local is_windows = volume_path ~= nil
if is_windows then
path = volume_path .. uri_encode(fname:gsub('\\', '/'))
else
path = uri_encode(path)
end
local uri_parts = { 'file://' }
if is_windows then
table.insert(uri_parts, '/')
end
table.insert(uri_parts, path)
return table.concat(uri_parts)
end
local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):.*'
local WINDOWS_URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):[a-zA-Z]:.*'
--- Get a URI from a bufnr
---@param bufnr integer
---@return string URI
local function uri_from_bufnr(bufnr)
local fname = vim.api.nvim_buf_get_name(bufnr)
local volume_path = fname:match('^([a-zA-Z]:).*')
local is_windows = volume_path ~= nil
local scheme
if is_windows then
fname = fname:gsub('\\', '/')
scheme = fname:match(WINDOWS_URI_SCHEME_PATTERN)
else
scheme = fname:match(URI_SCHEME_PATTERN)
end
if scheme then
return fname
else
return uri_from_fname(fname)
end
end
--- Get a filename from a URI
---@param uri string
---@return string filename or unchanged URI for non-file URIs
local function uri_to_fname(uri)
local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri)
if scheme ~= 'file' then
return uri
end
uri = uri_decode(uri)
-- TODO improve this.
if is_windows_file_uri(uri) then
uri = uri:gsub('^file:/+', '')
uri = uri:gsub('/', '\\')
else
uri = uri:gsub('^file:/+', '/')
end
return uri
end
--- Get the buffer for a uri.
--- Creates a new unloaded buffer if no buffer for the uri already exists.
--
---@param uri string
---@return integer bufnr
local function uri_to_bufnr(uri)
return vim.fn.bufadd(uri_to_fname(uri))
end
return {
uri_from_fname = uri_from_fname,
uri_from_bufnr = uri_from_bufnr,
uri_to_fname = uri_to_fname,
uri_to_bufnr = uri_to_bufnr,
}
-- vim:sw=2 ts=2 et
|