mirror of
https://github.com/neovim/neovim.git
synced 2026-11-06 05:16:52 +00:00
feat(uri): structured querystring, join(), new(), normalize() #42705
Problem: - vim.uri.parse() not fully baked, it does not structure some parts of its result (esp. `query`). - The scheme is not lowercased, which violates the RFC. - No "constructor" for vim.Uri. - tostring(vim.Uri) is not fully driven by the vim.Uri fields. Solution: - Structure `authority`, `query`. - Lowercase the scheme. - Drop `_parse_ssh_uri()`. - Add `vim.uri.new()`. - Fix tostring(vim.Uri). - Validation.
This commit is contained in:
1 parent
094621e4c9
commit
67f6f0503c
11 files changed
+883
-242
No files matched your search
+164
-26
@@ -2871,6 +2871,8 @@ vim.fs.joinpath({...}) *vim.fs.joinpath()*
|
||||
removed, and (on Windows) backslashes are replaced with forward-slashes.
|
||||
Empty segments are removed. Paths are not expanded/resolved.
|
||||
|
||||
To join URI paths, see |vim.uri.join()|.
|
||||
|
||||
Examples:
|
||||
• "foo/", "/bar" => "foo/bar"
|
||||
• "", "after/plugin" => "after/plugin"
|
||||
@@ -5886,42 +5888,106 @@ vim.ui.img.set({data_or_id}, {opts}) *vim.ui.img.set()*
|
||||
Lua module: vim.uri *vim.uri*
|
||||
|
||||
vim.Uri *vim.Uri*
|
||||
URI components (see |vim.uri.parse()|). `tostring()` gives the URI as a
|
||||
string. Can be compared for equality (`==`): >lua
|
||||
vim.uri.parse('HTTP://h/a') == vim.uri.new({ scheme = 'http', host = 'h', path = '/a' }) --> true
|
||||
<
|
||||
|
||||
Fields: ~
|
||||
• {authority}? (`string`) Authority (after "//"), e.g.
|
||||
"user@example.com:8080". Empty for "file:///a", nil for
|
||||
"file:/a".
|
||||
• {fragment}? (`string`) Fragment (after "#"), e.g. "section".
|
||||
• {path} (`string`) Path, e.g. "/a/b". Empty for
|
||||
"https://example.com".
|
||||
• {query}? (`string`) Query (after "?"), e.g. "q=1".
|
||||
• {scheme} (`string`) Scheme, e.g. "https".
|
||||
• {fragment}? (`string`) Fragment (URI-encoded): "https://h#a%20b" =>
|
||||
"a%20b".
|
||||
• {host}? (`string`) Host (lowercase, URI-decoded):
|
||||
`ssh://Me@[::1]:22` => "::1", "https://Example.com" =>
|
||||
"example.com". Empty for "file:///a", nil for "file:/a".
|
||||
• {password}? (`string`) Password (URI-decoded):
|
||||
"https://u:p%40ss@host" => "p@ss".
|
||||
• {path} (`string`) Path (URI-encoded): "https://h/a%20b" =>
|
||||
"/a%20b". Empty for "https://h".
|
||||
• {port}? (`integer`) Port: "https://example.com:8080" => 8080.
|
||||
• {query}? (`table<string, string|string[]>`) Query (decoded by
|
||||
|vim.uri.decode_query()|): "https://h?q=a+b" =>
|
||||
`{ q = "a b" }`.
|
||||
• {querystr}? (`string`) Querystring (URI-encoded): "https://h?q=a+b"
|
||||
=> "q=a+b". Takes precedence over `query` in
|
||||
`tostring()`.
|
||||
• {scheme} (`string`) Scheme (lowercase): "HTTPS://example.com" =>
|
||||
"https".
|
||||
• {user}? (`string`) User (URI-decoded): "ssh://a%40b@host" =>
|
||||
"a@b".
|
||||
|
||||
|
||||
vim.uri.decode({str}) *vim.uri.decode()*
|
||||
Decodes a URI-encoded string.
|
||||
Decodes a URI-encoded string. (Use |vim.uri.parse()| to get the structured
|
||||
form.)
|
||||
|
||||
Attributes: ~
|
||||
Since: 0.13.0
|
||||
|
||||
Parameters: ~
|
||||
• {str} (`string`) string to decode
|
||||
• {str} (`string`) URI-encoded string.
|
||||
|
||||
Return: ~
|
||||
(`string`) decoded string
|
||||
(`string`) Decoded string.
|
||||
|
||||
vim.uri.decode_query({query}) *vim.uri.decode_query()*
|
||||
Decodes a querystring (`application/x-www-form-urlencoded`, as HTML forms
|
||||
send it): "+" is space, and a repeated key gives a list.
|
||||
|
||||
Example: >lua
|
||||
vim.uri.decode_query('q=a+b%26c&tag=x&tag=y&flag')
|
||||
--> { q = 'a b&c', tag = { 'x', 'y' }, flag = '' }
|
||||
<
|
||||
|
||||
Attributes: ~
|
||||
Since: 0.13.0
|
||||
|
||||
Parameters: ~
|
||||
• {query} (`string?`) Querystring, e.g. a form body.
|
||||
|
||||
Return: ~
|
||||
(`table<string, string|string[]>`)
|
||||
|
||||
vim.uri.encode({str}, {rfc}) *vim.uri.encode()*
|
||||
URI-encodes a string.
|
||||
URI-encodes (percent-encodes) a string. For a querystring, use
|
||||
|vim.uri.encode_query()|.
|
||||
|
||||
Example: >lua
|
||||
vim.uri.encode('/a b?') --> '/a%20b%3f'
|
||||
vim.uri.encode('/a b?', 'rfc2396') --> '%2fa%20b%3f'
|
||||
<
|
||||
|
||||
Attributes: ~
|
||||
Since: 0.13.0
|
||||
|
||||
Parameters: ~
|
||||
• {str} (`string`) string to encode
|
||||
• {rfc} (`"rfc2396"|"rfc2732"|"rfc3986"?`)
|
||||
• {str} (`string`) Arbitrary string.
|
||||
• {rfc} (`"rfc2396"|"rfc2732"|"rfc3986"?`) Chars to keep:
|
||||
• "rfc2396": only `A-Za-z0-9-_.!~*'()`.
|
||||
• "rfc2732": "rfc2396" plus `[` and `]`.
|
||||
• "rfc3986" (default): those valid in a path, e.g. "/", ":",
|
||||
"@", "&", "=".
|
||||
|
||||
Return: ~
|
||||
(`string`) encoded string
|
||||
(`string`) URI-encoded string.
|
||||
|
||||
vim.uri.encode_query({params}) *vim.uri.encode_query()*
|
||||
Encodes `params` as a querystring (`application/x-www-form-urlencoded`, as
|
||||
HTML forms send it): space is "+", and a list value repeats its key. Sorts
|
||||
the keys.
|
||||
|
||||
Example: >lua
|
||||
vim.uri.encode_query({ q = 'a b&c', tag = { 'x', 'y' }, n = 1 })
|
||||
--> 'n=1&q=a+b%26c&tag=x&tag=y'
|
||||
<
|
||||
|
||||
Attributes: ~
|
||||
Since: 0.13.0
|
||||
|
||||
Parameters: ~
|
||||
• {params} (`table<string, string|number|boolean|(string|number|boolean)[]>`)
|
||||
|
||||
Return: ~
|
||||
(`string`)
|
||||
|
||||
vim.uri.from_buf({buf}) *vim.uri.from_buf()*
|
||||
Gets a URI from a buffer.
|
||||
@@ -5936,14 +6002,16 @@ vim.uri.from_buf({buf}) *vim.uri.from_buf()*
|
||||
(`string`) URI
|
||||
|
||||
vim.uri.from_fname({path}) *vim.uri.from_fname()*
|
||||
Gets a URI from a file path. Relative paths are resolved to
|
||||
|current-directory| (RFC 8089 disallows relative URIs).
|
||||
Gets a URI from a file path, or returns `path` if already a URI. Relative
|
||||
paths are resolved to |current-directory| (RFC 8089 disallows relative
|
||||
URIs).
|
||||
|
||||
Example: >lua
|
||||
vim.uri.from_fname('/a b/c#') --> 'file:///a%20b/c%23'
|
||||
vim.uri.from_fname('C:\\a\\b') --> 'file:///C:/a/b'
|
||||
vim.uri.from_fname('//server/share/a') --> 'file://server/share/a' (UNC)
|
||||
vim.uri.from_fname('a') --> 'file:///home/user/a' (if CWD is "/home/user")
|
||||
vim.uri.from_fname('jdt://a/b') --> 'jdt://a/b'
|
||||
<
|
||||
|
||||
Attributes: ~
|
||||
@@ -5955,16 +6023,86 @@ vim.uri.from_fname({path}) *vim.uri.from_fname()*
|
||||
Return: ~
|
||||
(`string`) URI
|
||||
|
||||
vim.uri.parse({uri}) *vim.uri.parse()*
|
||||
Parses a URI into its RFC 3986 components, without decoding them. Unlike
|
||||
RFC 3986, a one-letter scheme is a drive letter, so `C:/a` is not a URI.
|
||||
vim.uri.join({base}, {...}) *vim.uri.join()*
|
||||
Appends path segments to the path of `base`, like |vim.fs.joinpath()|, and
|
||||
removes "." and ".." segments, but never above the root or a Windows drive
|
||||
letter. Each segment is URI-encoded, so "#" or "?" in a file name is part
|
||||
of the path.
|
||||
|
||||
Example: >lua
|
||||
vim.uri.parse('https://example.com/a%20b?q=1#section')
|
||||
--> { scheme = 'https', authority = 'example.com', path = '/a%20b', query = 'q=1', fragment = 'section' }
|
||||
tostring(vim.uri.join('file:///home/u/proj', 'src', 'a#1.lua'))
|
||||
--> 'file:///home/u/proj/src/a%231.lua'
|
||||
tostring(vim.uri.join('file:///C:/a', '../../x')) --> 'file:///C:/x'
|
||||
tostring(vim.uri.join('https://h/api/?v=1', 'users/')) --> 'https://h/api/users/?v=1'
|
||||
vim.uri.join('file:///a', 'file:///b') --> error: a segment can't be a URI
|
||||
<
|
||||
|
||||
Attributes: ~
|
||||
Since: 0.13.0
|
||||
|
||||
Parameters: ~
|
||||
• {base} (`string|vim.Uri`)
|
||||
• {...} (`string`) Path segments (not URI-encoded), not URIs.
|
||||
|
||||
Return: ~
|
||||
(`vim.Uri`)
|
||||
|
||||
vim.uri.new({uri}) *vim.uri.new()*
|
||||
Creates a |vim.Uri| from components.
|
||||
|
||||
Example: >lua
|
||||
tostring(vim.uri.new({ scheme = 'https', host = '::1', port = 8080, path = '/a' }))
|
||||
--> 'https://[::1]:8080/a'
|
||||
tostring(vim.uri.new({ scheme = 'https', host = 'h', path = '/s', query = { q = 'a b' } }))
|
||||
--> 'https://h/s?q=a+b'
|
||||
<
|
||||
|
||||
Attributes: ~
|
||||
Since: 0.13.0
|
||||
|
||||
Parameters: ~
|
||||
• {uri} (`vim.Uri`) `path` defaults to "".
|
||||
|
||||
Return: ~
|
||||
(`vim.Uri`)
|
||||
|
||||
vim.uri.normalize({uri}) *vim.uri.normalize()*
|
||||
Normalizes `uri` (RFC 3986 6), so equivalent URIs are equal (`==`).
|
||||
Besides the normalizations of |vim.uri.parse()| and `tostring()`, it
|
||||
normalizes percent-encoding, removes "." and ".." segments, and drops a
|
||||
default port.
|
||||
|
||||
Example: >lua
|
||||
tostring(vim.uri.normalize('HTTPS://Example.COM:443/a/./b/../%7e%2f'))
|
||||
--> 'https://example.com/a/~%2F'
|
||||
vim.uri.normalize('HTTP://h:80') == vim.uri.normalize('http://h/') --> true
|
||||
<
|
||||
|
||||
Attributes: ~
|
||||
Since: 0.13.0
|
||||
|
||||
Parameters: ~
|
||||
• {uri} (`string|vim.Uri`)
|
||||
|
||||
Return: ~
|
||||
(`vim.Uri`)
|
||||
|
||||
vim.uri.parse({uri}) *vim.uri.parse()*
|
||||
Parses a URI into its RFC 3986 components. Exception: a one-letter scheme
|
||||
is a drive letter, "C:/a" is not a URI.
|
||||
|
||||
Example: >lua
|
||||
vim.uri.parse('https://u:p%40ss@example.com:80/a%20b?q=1#section')
|
||||
--> { scheme = 'https', user = 'u', password = 'p@ss', host = 'example.com', port = 80,
|
||||
path = '/a%20b', query = { q = '1' }, querystr = 'q=1', fragment = 'section' }
|
||||
vim.uri.parse('/a/b') --> nil
|
||||
vim.uri.parse('C:/a') --> nil
|
||||
tostring(vim.uri.parse('file:///a?b')) --> 'file:///a?b'
|
||||
vim.uri.parse('https://h:x') --> nil
|
||||
|
||||
-- tostring() builds the URI from the fields.
|
||||
local u = vim.uri.parse('https://example.com/a')
|
||||
u.host, u.port = '::1', 8080
|
||||
tostring(u) --> 'https://[::1]:8080/a'
|
||||
<
|
||||
|
||||
Attributes: ~
|
||||
@@ -5974,7 +6112,7 @@ vim.uri.parse({uri}) *vim.uri.parse()*
|
||||
• {uri} (`string`)
|
||||
|
||||
Return: ~
|
||||
(`vim.Uri?`) URI components, or nil if `uri` has no scheme.
|
||||
(`vim.Uri?`) URI components, or nil if invalid.
|
||||
|
||||
vim.uri.to_buf({uri}) *vim.uri.to_buf()*
|
||||
Gets the buffer for a uri. Creates a new unloaded buffer if no buffer for
|
||||
@@ -5984,7 +6122,7 @@ vim.uri.to_buf({uri}) *vim.uri.to_buf()*
|
||||
Since: 0.13.0
|
||||
|
||||
Parameters: ~
|
||||
• {uri} (`string`)
|
||||
• {uri} (`string|vim.Uri`)
|
||||
|
||||
Return: ~
|
||||
(`integer`) buf
|
||||
@@ -6004,7 +6142,7 @@ vim.uri.to_fname({uri}) *vim.uri.to_fname()*
|
||||
Since: 0.13.0
|
||||
|
||||
Parameters: ~
|
||||
• {uri} (`string`)
|
||||
• {uri} (`string|vim.Uri`)
|
||||
|
||||
Return: ~
|
||||
(`string`) filename or unchanged URI for non-file URIs
|
||||
|
||||
@@ -161,7 +161,7 @@ EVENTS
|
||||
• |BufModifiedSet| has been removed. Use the |OptionSet| event with pattern
|
||||
"modified" instead.
|
||||
• |ui-messages| `msg_show.completion` messages are now `msg_show.progress`
|
||||
events. If :write or :edit is slow it emits `msg_show.progress` events.
|
||||
events.
|
||||
|
||||
LSP
|
||||
|
||||
@@ -453,6 +453,11 @@ LUA
|
||||
• |vim.ui.input()| now allows setting input scope.
|
||||
• |vim.uri| is a module. The old `uri_xx` top-level functions were renamed,
|
||||
e.g. `vim.uri_to_fname()` is now |vim.uri.to_fname()|.
|
||||
• Parse/build URIs with |vim.uri.parse()|, |vim.uri.new()|,
|
||||
|vim.uri.join()|.
|
||||
• Check equality with |vim.uri.normalize()|.
|
||||
• Parse/build querystrings with |vim.uri.encode_query()| and
|
||||
|vim.uri.decode_query()|.
|
||||
• |vim.log| provides a logging interface.
|
||||
• |vim.pack.get()| output includes revision of a pending update.
|
||||
• |vim.pack.get()| can fetch new updates before computing the output.
|
||||
@@ -492,6 +497,7 @@ OPTIONS
|
||||
|
||||
PERFORMANCE
|
||||
|
||||
• If :write or :edit is slow it emits `msg_show.progress` events.
|
||||
• LSP shares identical file watcher registrations, reducing startup work in
|
||||
large workspaces. On macOS and Windows, watchers with different glob
|
||||
patterns share one native watcher per directory, which reduces cleanup work
|
||||
|
||||
@@ -133,6 +133,8 @@ end
|
||||
--- paths). Slashes are normalized: redundant slashes are removed, and (on Windows) backslashes are
|
||||
--- replaced with forward-slashes. Empty segments are removed. Paths are not expanded/resolved.
|
||||
---
|
||||
--- To join URI paths, see |vim.uri.join()|.
|
||||
---
|
||||
--- Examples:
|
||||
--- - "foo/", "/bar" => "foo/bar"
|
||||
--- - "", "after/plugin" => "after/plugin"
|
||||
@@ -196,7 +198,7 @@ function M.slug(path, opts)
|
||||
|
||||
-- Capture URI scheme before normalize() collapses "://" to ":/" (which looks like "C:/")
|
||||
local uri = uri_parse(path)
|
||||
local uri_scheme = uri and uri.authority and uri.scheme
|
||||
local uri_scheme = uri and uri.host and uri.scheme
|
||||
|
||||
-- Normalize before computing the hash so equivalent paths produce the same result
|
||||
path = vim.fs.normalize(path, { plain = true })
|
||||
@@ -664,7 +666,7 @@ function M.root(source, marker)
|
||||
error('invalid type for argument "source": expected string or buffer number')
|
||||
end
|
||||
local uri = uri_parse(path)
|
||||
if uri and uri.scheme:lower() == 'file' then
|
||||
if uri and uri.scheme == 'file' then
|
||||
path = vim.uri.to_fname(path)
|
||||
elseif is_url(path) then
|
||||
path = assert(uv.cwd())
|
||||
|
||||
@@ -620,10 +620,10 @@ local function check_external_tools()
|
||||
return
|
||||
end
|
||||
if vim.version.le(curl_version, { 7, 12, 3 }) then
|
||||
health.warn(('curl version %s not compatible'):format(curl_version))
|
||||
health.warn(('curl version %s not compatible'):format(tostring(curl_version)))
|
||||
return
|
||||
end
|
||||
local lines = { string.format('curl %s (%s)', curl_version, curl_path) }
|
||||
local lines = { ('curl %s (%s)'):format(tostring(curl_version), curl_path) }
|
||||
|
||||
for line in vim.gsplit(curl_out, '\n', { plain = true }) do
|
||||
if line ~= '' then
|
||||
@@ -864,8 +864,9 @@ local function check_sysinfo()
|
||||
if win == -1 then
|
||||
return
|
||||
end
|
||||
local encoded_body = vim.uri.encode(body) --- @type string
|
||||
local issue_url = 'https://github.com/neovim/neovim/issues/new?type=Bug&body=' .. encoded_body
|
||||
local issue_url = ('https://github.com/neovim/neovim/issues/new?%s'):format(
|
||||
vim.uri.encode_query({ type = 'Bug', body = body })
|
||||
)
|
||||
|
||||
--- Opens the prefilled issue from the checkhealth winbar.
|
||||
---@diagnostic disable-next-line: global-in-non-module
|
||||
|
||||
@@ -1122,12 +1122,13 @@ function Client:_get_registrations(provider, bufnr)
|
||||
else
|
||||
local language = self:_get_language_id(bufnr)
|
||||
local uri = vim.uri.from_buf(bufnr)
|
||||
local scheme = assert(vim.uri.parse(uri)).scheme
|
||||
local fname = vim.uri.to_fname(uri)
|
||||
for _, filter in ipairs(regoptions.documentSelector) do
|
||||
local flang, fscheme, fpat = filter.language, filter.scheme, filter.pattern
|
||||
if
|
||||
not (flang and language ~= flang)
|
||||
and not (fscheme and not vim.startswith(uri, fscheme .. ':'))
|
||||
and not (fscheme and fscheme:lower() ~= scheme)
|
||||
and not (type(fpat) == 'string' and not self:_glob_matches(fpat, fname))
|
||||
then
|
||||
matched_regs[#matched_regs + 1] = reg
|
||||
|
||||
@@ -320,7 +320,7 @@ end
|
||||
---
|
||||
--- `ssh_args` are inserted before the target; `remote_cmd` is appended after the target.
|
||||
---
|
||||
---@param ssh_uri {host:string, user?:string, port?:string, config?:string}
|
||||
---@param ssh_uri vim.Uri & {host:string, config?:string}
|
||||
---@param opts? { ssh_args?: string[], remote_cmd?: string[] }
|
||||
---@return string[]
|
||||
local function get_ssh_cmd(ssh_uri, opts)
|
||||
@@ -348,7 +348,7 @@ local function get_ssh_cmd(ssh_uri, opts)
|
||||
'RemoteCommand=none',
|
||||
})
|
||||
local target = ssh_uri.user and ('%s@%s'):format(ssh_uri.user, ssh_uri.host) or ssh_uri.host
|
||||
local dest = ssh_uri.port and { '-p', ssh_uri.port } or {}
|
||||
local dest = ssh_uri.port and { '-p', tostring(ssh_uri.port) } or {}
|
||||
-- Ends option parsing, so a target like `-oProxyCommand=x` is not taken as an option.
|
||||
vim.list_extend(dest, { '--', target })
|
||||
|
||||
@@ -384,7 +384,7 @@ end
|
||||
--- Runs the local `$VIMRUNTIME/scripts/remote_ssh.sh` on the remote host. The script goes to
|
||||
--- `sh -s` over stdin, so the remote login shell (any shell) parses only `args`.
|
||||
---
|
||||
---@param uri {host:string, user?:string, port?:string}
|
||||
---@param uri vim.Uri & {host:string}
|
||||
---@param args string[] Script args: plain words, which no shell expands, so they need no quoting.
|
||||
---@param opts? { ssh_args?: string[] }
|
||||
---@param on_exit? fun(out: vim.SystemCompleted)
|
||||
@@ -404,7 +404,7 @@ end
|
||||
|
||||
--- Gets the operating system and architecture from the remote system.
|
||||
---
|
||||
---@param uri {host:string, user?:string, port?:string}
|
||||
---@param uri vim.Uri & {host:string}
|
||||
---@return string os, string arch
|
||||
function M.get_system_info(uri)
|
||||
local ssh_cmd = get_ssh_cmd(uri, { remote_cmd = { 'uname -s && uname -m' } })
|
||||
@@ -497,7 +497,11 @@ end
|
||||
---@param chan? integer
|
||||
---@param report fun(status: 'running'|'success'|'failed', msg: string)
|
||||
local function connect(uri_str, chan, report)
|
||||
local uri = require('vim.uri')._parse_ssh_uri(uri_str)
|
||||
local uri = vim.uri.parse(uri_str)
|
||||
if not (uri and uri.host and uri.host ~= '') then
|
||||
error(('No host in SSH URI: %s'):format(uri_str), 0)
|
||||
end
|
||||
---@cast uri vim.Uri & {host:string}
|
||||
report('running', ('Connecting to %s...'):format(uri_str))
|
||||
local dir, url, version = get_release(M.get_system_info(uri))
|
||||
|
||||
|
||||
+458
-120
@@ -6,7 +6,6 @@
|
||||
local M = {}
|
||||
local sbyte = string.byte
|
||||
local schar = string.char
|
||||
local tohex = require('bit').tohex
|
||||
local iswin = package.config:sub(1, 1) == '\\'
|
||||
local PATTERNS = {
|
||||
-- RFC 2396 https://tools.ietf.org/html/rfc2396#section-2.3
|
||||
@@ -16,74 +15,102 @@ local PATTERNS = {
|
||||
-- RFC 3986 https://tools.ietf.org/html/rfc3986#section-3.3
|
||||
rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/",
|
||||
}
|
||||
-- Default ports of the WHATWG URL "special" schemes.
|
||||
local default_ports = { ftp = 21, http = 80, https = 443, ws = 80, wss = 443 }
|
||||
-- RFC 3986 "unreserved" and "sub-delims" chars (Lua pattern set).
|
||||
local authority_chars = "A-Za-z0-9%-._~!$&'()*+,;="
|
||||
-- Scheme (Lua pattern). Requires 2+ chars, so drive letters ("C:/a") are not schemes.
|
||||
local scheme_pat = '%a[%w+.-]+'
|
||||
|
||||
---Converts hex to char
|
||||
---@param hex string
|
||||
--- Encodes a user, password, or host: keeps only `authority_chars`. Uses uppercase hex (RFC 3986
|
||||
--- 2.1).
|
||||
---@param s string
|
||||
---@return string
|
||||
local function hex_to_char(hex)
|
||||
return schar(vim._assert_integer(hex, 16))
|
||||
local function encode_authority_part(s)
|
||||
return (
|
||||
s:gsub(('[^%s]'):format(authority_chars), function(c)
|
||||
return ('%%%02X'):format(sbyte(c))
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
---@param char string
|
||||
---@return string
|
||||
local function percent_encode_char(char)
|
||||
return '%' .. tohex(sbyte(char), 2)
|
||||
--- Checks that `s` has only `chars` (Lua pattern set), "%XX", and non-ASCII bytes (an unencoded
|
||||
--- international name).
|
||||
---@param s string
|
||||
---@param chars string
|
||||
---@return boolean
|
||||
local function has_only(s, chars)
|
||||
return not s:gsub('%%%x%x', ''):find(('[^%s\128-\255]'):format(chars))
|
||||
end
|
||||
|
||||
--- Gets the querystring of `uri`: `querystr`, else the encoded `query`.
|
||||
---@param uri vim.Uri
|
||||
---@return string?
|
||||
local function query_string(uri)
|
||||
if uri.querystr or not (uri.query and next(uri.query)) then
|
||||
return uri.querystr
|
||||
end
|
||||
return M.encode_query(uri.query)
|
||||
end
|
||||
|
||||
local uri_mt = {
|
||||
--- @param uri vim.Uri
|
||||
--- @return string
|
||||
__tostring = function(uri)
|
||||
local authority --- @type string?
|
||||
if uri.host then
|
||||
local password = uri.password and (':%s'):format(encode_authority_part(uri.password)) or ''
|
||||
local userinfo = uri.user and ('%s%s@'):format(encode_authority_part(uri.user), password)
|
||||
or ''
|
||||
-- IPv6 in brackets, "%" of a zone ID encoded: "fe80::1%en0" => "[fe80::1%25en0]".
|
||||
local host = uri.host:find(':', 1, true) and ('[%s]'):format((uri.host:gsub('%%', '%%25')))
|
||||
or encode_authority_part(uri.host)
|
||||
local port = uri.port and (':%d'):format(uri.port) or ''
|
||||
authority = ('%s%s%s'):format(userinfo, host, port)
|
||||
end
|
||||
local query = query_string(uri)
|
||||
return ('%s:%s%s%s%s'):format(
|
||||
uri.scheme,
|
||||
uri.authority and ('//%s'):format(uri.authority) or '',
|
||||
authority and ('//%s'):format(authority) or '',
|
||||
uri.path,
|
||||
uri.query and ('?%s'):format(uri.query) or '',
|
||||
query and ('?%s'):format(query) or '',
|
||||
uri.fragment and ('#%s'):format(uri.fragment) or ''
|
||||
)
|
||||
end,
|
||||
--- @param a vim.Uri
|
||||
--- @param b vim.Uri
|
||||
--- @return boolean
|
||||
__eq = function(a, b)
|
||||
return tostring(a) == tostring(b)
|
||||
end,
|
||||
}
|
||||
|
||||
--- @class vim.Uri
|
||||
---
|
||||
--- Scheme, e.g. "https".
|
||||
--- @field scheme string
|
||||
---
|
||||
--- Authority (after "//"), e.g. "user@example.com:8080". Empty for "file:///a", nil for "file:/a".
|
||||
--- @field authority? string
|
||||
---
|
||||
--- Path, e.g. "/a/b". Empty for "https://example.com".
|
||||
--- @field path string
|
||||
---
|
||||
--- Query (after "?"), e.g. "q=1".
|
||||
--- @field query? string
|
||||
---
|
||||
--- Fragment (after "#"), e.g. "section".
|
||||
--- @field fragment? string
|
||||
--- Creates a |vim.Uri| from unvalidated fields.
|
||||
---@param fields vim.Uri `path` defaults to "".
|
||||
---@return vim.Uri
|
||||
local function _new(fields)
|
||||
return setmetatable({
|
||||
-- Lowercase, as specified by RFC.
|
||||
scheme = fields.scheme:lower(),
|
||||
user = fields.user,
|
||||
password = fields.password,
|
||||
host = fields.host and fields.host:lower(),
|
||||
port = fields.port,
|
||||
path = fields.path or '',
|
||||
query = fields.query,
|
||||
querystr = fields.querystr,
|
||||
fragment = fields.fragment,
|
||||
}, uri_mt)
|
||||
end
|
||||
|
||||
--- Parses a URI into its RFC 3986 components, without decoding them. Unlike RFC 3986, a one-letter
|
||||
--- scheme is a drive letter, so `C:/a` is not a URI.
|
||||
---
|
||||
--- Example:
|
||||
---
|
||||
--- ```lua
|
||||
--- vim.uri.parse('https://example.com/a%20b?q=1#section')
|
||||
--- --> { scheme = 'https', authority = 'example.com', path = '/a%20b', query = 'q=1', fragment = 'section' }
|
||||
--- vim.uri.parse('/a/b') --> nil
|
||||
--- vim.uri.parse('C:/a') --> nil
|
||||
--- tostring(vim.uri.parse('file:///a?b')) --> 'file:///a?b'
|
||||
--- ```
|
||||
---
|
||||
---@since 15
|
||||
---@param uri string
|
||||
---@return vim.Uri? # URI components, or nil if `uri` has no scheme.
|
||||
function M.parse(uri)
|
||||
-- Requires 2+ chars, so drive letters ("C:/a") are not schemes.
|
||||
local scheme = uri:match('^(%a[%w+.-]+):')
|
||||
if not scheme then
|
||||
return nil
|
||||
end
|
||||
local rest = uri:sub(#scheme + 2)
|
||||
--- Parses a URI reference (URI or relative reference, RFC 3986 4.1) into |vim.Uri| fields, with a
|
||||
--- nil `scheme` for a relative reference: "../a?q" => `{ path = "../a", querystr = "q", … }`.
|
||||
---@param ref string
|
||||
---@return vim.Uri? # Fields, or nil if the authority is invalid.
|
||||
local function parse_ref(ref)
|
||||
-- Splits (RFC 3986 Appendix B).
|
||||
local scheme = ref:match(('^(%s):'):format(scheme_pat)) --- @type string?
|
||||
local rest = scheme and ref:sub(#scheme + 2) or ref
|
||||
local fragment_i = rest:find('#', 1, true)
|
||||
local fragment = fragment_i and rest:sub(fragment_i + 1) or nil
|
||||
rest = rest:sub(1, (fragment_i or 0) - 1)
|
||||
@@ -91,35 +118,377 @@ function M.parse(uri)
|
||||
local query = query_i and rest:sub(query_i + 1) or nil
|
||||
rest = rest:sub(1, (query_i or 0) - 1)
|
||||
local authority, path = rest:match('^//([^/]*)(.*)$') --- @type string?, string?
|
||||
return setmetatable({
|
||||
path = path or rest
|
||||
|
||||
-- Exception: a drive letter in the host position is not a host (like WHATWG URL):
|
||||
-- "file://C:/a" => host "", path "/C:/a". But "file://h:22/a" has a port.
|
||||
local decoded = scheme and scheme:lower() == 'file' and authority and M.decode(authority) or ''
|
||||
if decoded:find('^%a:$') or decoded:find('^%a:\\') then
|
||||
authority, path = '', ('/%s%s'):format(authority, path)
|
||||
end
|
||||
|
||||
local user, password, host, port --- @type string?, string?, string?, integer?
|
||||
if authority then
|
||||
local userinfo, hostport = authority:match('^(.*)@(.*)$') --- @type string?, string?
|
||||
hostport = hostport or authority
|
||||
-- IP literal in brackets: "[::1]:22" => "[::1]", ":22".
|
||||
local h, tail = hostport:match('^(%b[])(.*)$') --- @type string?, string?
|
||||
if not h then
|
||||
h, tail = hostport:match('^([^:]*)(.*)$')
|
||||
end
|
||||
---@cast h string
|
||||
---@cast tail string
|
||||
local p = tail:match('^:(%d*)$') --- @type string?
|
||||
local ip = h:match('^%[(.*)%]$') --- @type string?
|
||||
port = vim._tointeger(p)
|
||||
|
||||
-- Validation
|
||||
if
|
||||
not (tail == '' or p)
|
||||
or (port and port > 65535)
|
||||
or (userinfo and not has_only(userinfo, ('%s:'):format(authority_chars)))
|
||||
-- IPv6, with zone ID ("fe80::1%25en0"), or IPvFuture ("v1.x").
|
||||
or (ip and not (ip:find('^[%x:.]+$') or ip:find('^[%x:.]+%%25.') or ip:find('^v%x+%.')))
|
||||
or (not ip and not has_only(h, authority_chars))
|
||||
then
|
||||
return nil
|
||||
end
|
||||
|
||||
if userinfo then
|
||||
local u, pw = userinfo:match('^([^:]*):(.*)$') --- @type string?, string?
|
||||
user, password = M.decode(u or userinfo), pw and M.decode(pw)
|
||||
end
|
||||
-- Drops IPv6 brackets: "[::1]" => "::1".
|
||||
host = M.decode(ip or h)
|
||||
end
|
||||
return {
|
||||
scheme = scheme,
|
||||
authority = authority,
|
||||
path = path or rest,
|
||||
query = query,
|
||||
user = user,
|
||||
password = password,
|
||||
host = host,
|
||||
port = port,
|
||||
path = path,
|
||||
query = query and M.decode_query(query),
|
||||
querystr = query,
|
||||
fragment = fragment,
|
||||
}, uri_mt)
|
||||
} --[[@as vim.Uri]]
|
||||
end
|
||||
|
||||
---URI-encodes a string.
|
||||
--- URI components (see |vim.uri.parse()|). `tostring()` gives the URI as a string. Can be compared
|
||||
--- for equality (`==`):
|
||||
---
|
||||
--- ```lua
|
||||
--- vim.uri.parse('HTTP://h/a') == vim.uri.new({ scheme = 'http', host = 'h', path = '/a' }) --> true
|
||||
--- ```
|
||||
---
|
||||
--- @class vim.Uri
|
||||
---
|
||||
--- Scheme (lowercase): "HTTPS://example.com" => "https".
|
||||
--- @field scheme string
|
||||
---
|
||||
--- User (URI-decoded): "ssh://a%40b@host" => "a@b".
|
||||
--- @field user? string
|
||||
---
|
||||
--- Password (URI-decoded): "https://u:p%40ss@host" => "p@ss".
|
||||
--- @field password? string
|
||||
---
|
||||
--- Host (lowercase, URI-decoded): `ssh://Me@[::1]:22` => "::1", "https://Example.com" =>
|
||||
--- "example.com". Empty for "file:///a", nil for "file:/a".
|
||||
--- @field host? string
|
||||
---
|
||||
--- Port: "https://example.com:8080" => 8080.
|
||||
--- @field port? integer
|
||||
---
|
||||
--- Path (URI-encoded): "https://h/a%20b" => "/a%20b". Empty for "https://h".
|
||||
--- @field path string
|
||||
---
|
||||
--- Query (decoded by |vim.uri.decode_query()|): "https://h?q=a+b" => `{ q = "a b" }`.
|
||||
--- @field query? table<string, string|string[]>
|
||||
---
|
||||
--- Querystring (URI-encoded): "https://h?q=a+b" => "q=a+b". Takes precedence over `query` in
|
||||
--- `tostring()`.
|
||||
--- @field querystr? string
|
||||
---
|
||||
--- Fragment (URI-encoded): "https://h#a%20b" => "a%20b".
|
||||
--- @field fragment? string
|
||||
|
||||
--- Parses a URI into its RFC 3986 components. Exception: a one-letter scheme is a drive letter, "C:/a" is not a URI.
|
||||
---
|
||||
--- Example:
|
||||
---
|
||||
--- ```lua
|
||||
--- vim.uri.parse('https://u:p%40ss@example.com:80/a%20b?q=1#section')
|
||||
--- --> { scheme = 'https', user = 'u', password = 'p@ss', host = 'example.com', port = 80,
|
||||
--- path = '/a%20b', query = { q = '1' }, querystr = 'q=1', fragment = 'section' }
|
||||
--- vim.uri.parse('/a/b') --> nil
|
||||
--- vim.uri.parse('C:/a') --> nil
|
||||
--- vim.uri.parse('https://h:x') --> nil
|
||||
---
|
||||
--- -- tostring() builds the URI from the fields.
|
||||
--- local u = vim.uri.parse('https://example.com/a')
|
||||
--- u.host, u.port = '::1', 8080
|
||||
--- tostring(u) --> 'https://[::1]:8080/a'
|
||||
--- ```
|
||||
---
|
||||
---@since 15
|
||||
---@param str string string to encode
|
||||
---@param rfc "rfc2396" | "rfc2732" | "rfc3986" | nil
|
||||
---@return string encoded string
|
||||
---@param uri string
|
||||
---@return vim.Uri? # URI components, or nil if invalid.
|
||||
function M.parse(uri)
|
||||
local fields = parse_ref(uri)
|
||||
return fields and fields.scheme and _new(fields) or nil
|
||||
end
|
||||
|
||||
--- Creates a |vim.Uri| from components.
|
||||
---
|
||||
--- Example:
|
||||
---
|
||||
--- ```lua
|
||||
--- tostring(vim.uri.new({ scheme = 'https', host = '::1', port = 8080, path = '/a' }))
|
||||
--- --> 'https://[::1]:8080/a'
|
||||
--- tostring(vim.uri.new({ scheme = 'https', host = 'h', path = '/s', query = { q = 'a b' } }))
|
||||
--- --> 'https://h/s?q=a+b'
|
||||
--- ```
|
||||
---
|
||||
---@since 15
|
||||
---@param uri vim.Uri `path` defaults to "".
|
||||
---@return vim.Uri
|
||||
function M.new(uri)
|
||||
vim.validate('uri', uri, 'table')
|
||||
vim.validate('uri.scheme', uri.scheme, 'string')
|
||||
vim.validate('uri.port', uri.port, 'number', true)
|
||||
vim.validate('uri.query', uri.query, 'table', true)
|
||||
for _, k in ipairs({ 'user', 'password', 'host', 'path', 'querystr', 'fragment' }) do
|
||||
vim.validate(('uri.%s'):format(k), uri[k], 'string', true)
|
||||
end
|
||||
local u = _new(uri)
|
||||
u.query = u.query or (u.querystr and M.decode_query(u.querystr))
|
||||
return u
|
||||
end
|
||||
|
||||
--- Removes "." and ".." segments (RFC 3986 5.2.4): "/a/./b/../c" => "/a/c".
|
||||
---@param path string
|
||||
---@return string
|
||||
local function remove_dot_segments(path)
|
||||
local out = {} --- @type string[]
|
||||
while path ~= '' do
|
||||
local dots = path:match('^%.%.?/') --- @type string?
|
||||
if dots then
|
||||
path = path:sub(#dots + 1)
|
||||
elseif path == '/.' or path:find('^/%./') then
|
||||
path = ('/%s'):format(path:sub(4))
|
||||
elseif path == '/..' or path:find('^/%.%./') then
|
||||
path = ('/%s'):format(path:sub(5))
|
||||
out[#out] = nil
|
||||
elseif path == '.' or path == '..' then
|
||||
path = ''
|
||||
else
|
||||
local segment = path:match('^/?[^/]*') --[[@as string]]
|
||||
out[#out + 1] = segment
|
||||
path = path:sub(#segment + 1)
|
||||
end
|
||||
end
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
--- Appends path segments to the path of `base`, like |vim.fs.joinpath()|, and removes "." and ".."
|
||||
--- segments, but never above the root or a Windows drive letter. Each segment is URI-encoded, so
|
||||
--- "#" or "?" in a file name is part of the path.
|
||||
---
|
||||
--- Example:
|
||||
---
|
||||
--- ```lua
|
||||
--- tostring(vim.uri.join('file:///home/u/proj', 'src', 'a#1.lua'))
|
||||
--- --> 'file:///home/u/proj/src/a%231.lua'
|
||||
--- tostring(vim.uri.join('file:///C:/a', '../../x')) --> 'file:///C:/x'
|
||||
--- tostring(vim.uri.join('https://h/api/?v=1', 'users/')) --> 'https://h/api/users/?v=1'
|
||||
--- vim.uri.join('file:///a', 'file:///b') --> error: a segment can't be a URI
|
||||
--- ```
|
||||
---
|
||||
---@since 15
|
||||
---@param base string|vim.Uri
|
||||
---@param ... string Path segments (not URI-encoded), not URIs.
|
||||
---@return vim.Uri
|
||||
function M.join(base, ...)
|
||||
vim.validate('base', base, { 'string', 'table' })
|
||||
local u = type(base) == 'table' and _new(base)
|
||||
or M.parse(base --[[@as string]])
|
||||
or error(('Invalid URI: %s'):format(base))
|
||||
local path = u.path
|
||||
for i = 1, select('#', ...) do
|
||||
local segment = select(i, ...) --- @type string
|
||||
vim.validate('segment', segment, 'string')
|
||||
if segment:find(('^%s://'):format(scheme_pat)) then
|
||||
error(('Segment is a URI: %s'):format(segment))
|
||||
end
|
||||
path = ('%s/%s'):format(path, M.encode(segment))
|
||||
end
|
||||
path = path:gsub('/+', '/')
|
||||
-- Keeps a drive letter as the root: "/C:/a/../.." => "/C:/".
|
||||
local drive = u.scheme == 'file' and path:match('^/%a:') or ''
|
||||
u.path = ('%s%s'):format(drive, remove_dot_segments(path:sub(#drive + 1)))
|
||||
return u
|
||||
end
|
||||
|
||||
--- Normalizes percent-encoding (RFC 3986 6.2.2.2): decodes "unreserved" chars, and uppercases the
|
||||
--- hex of others: "%7e%2f" => "~%2F".
|
||||
---@param s string
|
||||
---@return string
|
||||
local function normalize_percent(s)
|
||||
return (
|
||||
s:gsub('%%(%x%x)', function(hex)
|
||||
local c = schar(vim._assert_integer(hex, 16))
|
||||
return c:find('^[A-Za-z0-9%-._~]$') and c or ('%%%s'):format(hex:upper())
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
--- Normalizes `uri` (RFC 3986 6), so equivalent URIs are equal (`==`). Besides the normalizations
|
||||
--- of |vim.uri.parse()| and `tostring()`, it normalizes percent-encoding, removes "." and ".."
|
||||
--- segments, and drops a default port.
|
||||
---
|
||||
--- Example:
|
||||
---
|
||||
--- ```lua
|
||||
--- tostring(vim.uri.normalize('HTTPS://Example.COM:443/a/./b/../%7e%2f'))
|
||||
--- --> 'https://example.com/a/~%2F'
|
||||
--- vim.uri.normalize('HTTP://h:80') == vim.uri.normalize('http://h/') --> true
|
||||
--- ```
|
||||
---
|
||||
---@since 15
|
||||
---@param uri string|vim.Uri
|
||||
---@return vim.Uri
|
||||
function M.normalize(uri)
|
||||
vim.validate('uri', uri, { 'string', 'table' })
|
||||
local u = type(uri) == 'table' and _new(uri)
|
||||
or M.parse(uri --[[@as string]])
|
||||
or error(('Invalid URI: %s'):format(uri))
|
||||
-- "file://localhost/a" => "file:///a" (RFC 8089 2).
|
||||
if u.scheme == 'file' and u.host == 'localhost' then
|
||||
u.host = ''
|
||||
end
|
||||
local default_port = default_ports[u.scheme]
|
||||
u.port = u.port ~= default_port and u.port or nil
|
||||
u.path = normalize_percent(u.path)
|
||||
if u.path:find('^/') then
|
||||
u.path = remove_dot_segments(u.path)
|
||||
elseif u.host and u.path == '' and default_port then
|
||||
-- Empty path of a "special" scheme: "http://h" => "http://h/".
|
||||
u.path = '/'
|
||||
end
|
||||
local querystr = query_string(u)
|
||||
u.querystr = querystr and normalize_percent(querystr)
|
||||
u.fragment = u.fragment and normalize_percent(u.fragment)
|
||||
return u
|
||||
end
|
||||
|
||||
--- URI-encodes (percent-encodes) a string. For a querystring, use |vim.uri.encode_query()|.
|
||||
---
|
||||
--- Example:
|
||||
---
|
||||
--- ```lua
|
||||
--- vim.uri.encode('/a b?') --> '/a%20b%3f'
|
||||
--- vim.uri.encode('/a b?', 'rfc2396') --> '%2fa%20b%3f'
|
||||
--- ```
|
||||
---
|
||||
---@since 15
|
||||
---@param str string Arbitrary string.
|
||||
---@param rfc? "rfc2396"|"rfc2732"|"rfc3986" Chars to keep:
|
||||
--- - "rfc2396": only `A-Za-z0-9-_.!~*'()`.
|
||||
--- - "rfc2732": "rfc2396" plus `[` and `]`.
|
||||
--- - "rfc3986" (default): those valid in a path, e.g. "/", ":", "@", "&", "=".
|
||||
---@return string # URI-encoded string.
|
||||
function M.encode(str, rfc)
|
||||
local pattern = PATTERNS[rfc] or PATTERNS.rfc3986
|
||||
return (str:gsub('([' .. pattern .. '])', percent_encode_char)) -- clamped to 1 retval with ()
|
||||
-- TODO(justinmk): Uppercase hex (RFC 3986 2.1), like `encode_authority_part()`? Would change
|
||||
-- `from_fname()` URIs of non-ASCII paths sent to LSP servers.
|
||||
return (
|
||||
str:gsub(('[%s]'):format(pattern), function(c)
|
||||
return ('%%%02x'):format(sbyte(c))
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
---Decodes a URI-encoded string.
|
||||
--- Encodes `params` as a querystring (`application/x-www-form-urlencoded`, as HTML forms send it):
|
||||
--- space is "+", and a list value repeats its key. Sorts the keys.
|
||||
---
|
||||
--- Example:
|
||||
---
|
||||
--- ```lua
|
||||
--- vim.uri.encode_query({ q = 'a b&c', tag = { 'x', 'y' }, n = 1 })
|
||||
--- --> 'n=1&q=a+b%26c&tag=x&tag=y'
|
||||
--- ```
|
||||
---
|
||||
---@since 15
|
||||
---@param str string string to decode
|
||||
---@return string decoded string
|
||||
function M.decode(str)
|
||||
return (str:gsub('%%([a-fA-F0-9][a-fA-F0-9])', hex_to_char)) -- clamped to 1 retval with ()
|
||||
---@param params table<string, string|number|boolean|(string|number|boolean)[]>
|
||||
---@return string
|
||||
function M.encode_query(params)
|
||||
vim.validate('params', params, 'table')
|
||||
-- Keeps only the chars that HTML forms keep: "a b&c" => "a+b%26c".
|
||||
local function encode(s)
|
||||
local encoded = tostring(s):gsub('[^A-Za-z0-9*%-._ ]', function(c)
|
||||
return ('%%%02X'):format(sbyte(c))
|
||||
end)
|
||||
return (encoded:gsub(' ', '+'))
|
||||
end
|
||||
local keys = vim.tbl_keys(params)
|
||||
table.sort(keys, function(a, b)
|
||||
return tostring(a) < tostring(b)
|
||||
end)
|
||||
local items = {} --- @type string[]
|
||||
for _, k in ipairs(keys) do
|
||||
local v = params[k]
|
||||
for _, item in ipairs(type(v) == 'table' and v or { v }) do
|
||||
items[#items + 1] = ('%s=%s'):format(encode(k), encode(item))
|
||||
end
|
||||
end
|
||||
return table.concat(items, '&')
|
||||
end
|
||||
|
||||
---Gets a URI from a file path. Relative paths are resolved to |current-directory| (RFC 8089
|
||||
---disallows relative URIs).
|
||||
--- Decodes a querystring (`application/x-www-form-urlencoded`, as HTML forms send it): "+" is
|
||||
--- space, and a repeated key gives a list.
|
||||
---
|
||||
--- Example:
|
||||
---
|
||||
--- ```lua
|
||||
--- vim.uri.decode_query('q=a+b%26c&tag=x&tag=y&flag')
|
||||
--- --> { q = 'a b&c', tag = { 'x', 'y' }, flag = '' }
|
||||
--- ```
|
||||
---
|
||||
---@since 15
|
||||
---@param query string? Querystring, e.g. a form body.
|
||||
---@return table<string, string|string[]>
|
||||
function M.decode_query(query)
|
||||
vim.validate('query', query, 'string', true)
|
||||
local params = {} --- @type table<string, string|string[]>
|
||||
for pair in (query or ''):gmatch('[^&]+') do
|
||||
pair = pair:gsub('%+', ' ')
|
||||
local eq_i = pair:find('=', 1, true)
|
||||
local k = M.decode(pair:sub(1, (eq_i or 0) - 1))
|
||||
local v = M.decode(eq_i and pair:sub(eq_i + 1) or '')
|
||||
local prev = params[k]
|
||||
if prev == nil then
|
||||
params[k] = v
|
||||
elseif type(prev) == 'table' then
|
||||
prev[#prev + 1] = v
|
||||
else
|
||||
params[k] = { prev, v }
|
||||
end
|
||||
end
|
||||
return params
|
||||
end
|
||||
|
||||
---Decodes a URI-encoded string. (Use |vim.uri.parse()| to get the structured form.)
|
||||
---@since 15
|
||||
---@param str string URI-encoded string.
|
||||
---@return string # Decoded string.
|
||||
function M.decode(str)
|
||||
return (str:gsub('%%(%x%x)', function(hex)
|
||||
return schar(vim._assert_integer(hex, 16))
|
||||
end))
|
||||
end
|
||||
|
||||
---Gets a URI from a file path, or returns `path` if already a URI. Relative paths are resolved to
|
||||
---|current-directory| (RFC 8089 disallows relative URIs).
|
||||
---
|
||||
---Example:
|
||||
---
|
||||
@@ -128,12 +497,16 @@ end
|
||||
---vim.uri.from_fname('C:\\a\\b') --> 'file:///C:/a/b'
|
||||
---vim.uri.from_fname('//server/share/a') --> 'file://server/share/a' (UNC)
|
||||
---vim.uri.from_fname('a') --> 'file:///home/user/a' (if CWD is "/home/user")
|
||||
---vim.uri.from_fname('jdt://a/b') --> 'jdt://a/b'
|
||||
---```
|
||||
---
|
||||
---@since 15
|
||||
---@param path string Path to file
|
||||
---@return string URI
|
||||
function M.from_fname(path)
|
||||
if M.parse(path) then
|
||||
return path
|
||||
end
|
||||
if iswin or path:find('^%a:') then
|
||||
path = path:gsub('\\', '/') ---@type string
|
||||
end
|
||||
@@ -142,13 +515,11 @@ function M.from_fname(path)
|
||||
end
|
||||
local host = path:match('^//([^/]+)') ---@type string?
|
||||
if host then -- UNC path.
|
||||
return ('file://%s%s'):format(M.encode(host), M.encode(path:sub(#host + 3)))
|
||||
path = path:sub(#host + 3)
|
||||
elseif path:find('^%a:') then -- Windows drive letter.
|
||||
path = ('/%s'):format(path)
|
||||
end
|
||||
local drive = path:match('^%a:') ---@type string?
|
||||
if drive then -- Windows drive letter.
|
||||
return ('file:///%s%s'):format(drive, M.encode(path:sub(3)))
|
||||
end
|
||||
return ('file://%s'):format(M.encode(path))
|
||||
return tostring(_new({ scheme = 'file', host = host or '', path = M.encode(path) }))
|
||||
end
|
||||
|
||||
---Gets a URI from a buffer.
|
||||
@@ -156,11 +527,7 @@ end
|
||||
---@param buf integer
|
||||
---@return string URI
|
||||
function M.from_buf(buf)
|
||||
local fname = vim.api.nvim_buf_get_name(buf)
|
||||
if M.parse(fname) then
|
||||
return fname
|
||||
end
|
||||
return M.from_fname(fname)
|
||||
return M.from_fname(vim.api.nvim_buf_get_name(buf))
|
||||
end
|
||||
|
||||
---Gets a filename from a URI. Drops the query and fragment, so "?" and "#" in a filename must be
|
||||
@@ -176,30 +543,29 @@ end
|
||||
---```
|
||||
---
|
||||
---@since 15
|
||||
---@param uri string
|
||||
---@param uri string|vim.Uri
|
||||
---@return string filename or unchanged URI for non-file URIs
|
||||
function M.to_fname(uri)
|
||||
local parts = M.parse(uri)
|
||||
if not parts and uri:find('^%a:') then -- Drive letter: already a filename.
|
||||
return uri
|
||||
local parts = type(uri) == 'table' and uri or M.parse(uri --[[@as string]])
|
||||
if not parts then
|
||||
local str = uri --[[@as string]]
|
||||
if not str:find('^%a:') then
|
||||
error(('Invalid URI: %s'):format(str))
|
||||
end
|
||||
return str -- Drive letter: already a filename.
|
||||
end
|
||||
parts = parts or error(('URI must contain a scheme: %s'):format(uri))
|
||||
if parts.scheme:lower() ~= 'file' then
|
||||
return uri
|
||||
if parts.scheme ~= 'file' then
|
||||
return tostring(uri)
|
||||
end
|
||||
local host = M.decode(parts.authority or '')
|
||||
local host = parts.host or ''
|
||||
local path = M.decode(parts.path)
|
||||
if host:find('^%a:') then
|
||||
-- Drive letter, not a host (like WHATWG URL): "file://C:/a" => "C:/a".
|
||||
host, path = '', ('%s%s'):format(host, path)
|
||||
end
|
||||
-- Collapses leading slashes: "file:a" => "/a", "file:////a" => "/a".
|
||||
-- TODO(justinmk): Error on a relative path ("file:a"), like Rust `Url::to_file_path()`, unless
|
||||
-- LSP servers send those?
|
||||
path = path:gsub('^/*', '/') ---@type string
|
||||
-- TODO: On Windows, accessing "//host/…" connects to the host (and may send NTLM credentials).
|
||||
-- If users report it, allowlist hosts via a new option (like VS Code "security.allowedUNCHosts").
|
||||
if host ~= '' and host:lower() ~= 'localhost' then -- UNC path.
|
||||
if host ~= '' and host ~= 'localhost' then -- UNC path.
|
||||
return ('//%s%s'):format(host, path)
|
||||
end
|
||||
-- Drive letter: "/C:/a" => "C:/a".
|
||||
@@ -209,38 +575,10 @@ end
|
||||
---Gets the buffer for a uri.
|
||||
---Creates a new unloaded buffer if no buffer for the uri already exists.
|
||||
---@since 15
|
||||
---@param uri string
|
||||
---@param uri string|vim.Uri
|
||||
---@return integer buf
|
||||
function M.to_buf(uri)
|
||||
return vim.fn.bufadd(M.to_fname(uri))
|
||||
end
|
||||
|
||||
---@class vim.net.SshUri
|
||||
---@nodoc
|
||||
---@field host string
|
||||
---@field user? string
|
||||
---@field port? string
|
||||
|
||||
--- Splits `ssh://[user@]host[:port][/path]` or `[user@]host[:port]`. Drops the path, and the
|
||||
--- brackets of an IPv6 host: `ssh://u@[::1]:22/` => `{ user = 'u', host = '::1', port = '22' }`.
|
||||
--- Percent-decodes the user and host: `ssh://a%40b@h` => `{ user = 'a@b', host = 'h' }`. Fails if
|
||||
--- the host is empty.
|
||||
---@param str string
|
||||
---@return vim.net.SshUri
|
||||
function M._parse_ssh_uri(str)
|
||||
local parsed = M.parse(str)
|
||||
local authority = parsed and parsed.authority or str
|
||||
local user, host = authority:match('^(.*)@(.*)$') --- @type string?, string?
|
||||
host = host or authority
|
||||
local port = host:match(':(%d+)$') --- @type string?
|
||||
if port then
|
||||
host = host:sub(1, -#port - 2)
|
||||
end
|
||||
host = M.decode(host:match('^%[(.*)%]$') or host)
|
||||
if host == '' then
|
||||
error(('No host in SSH URI: %s'):format(str), 0)
|
||||
end
|
||||
return { host = host, user = user and M.decode(user), port = port }
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -33,6 +33,7 @@ describe('luaeval(vim.api.…)', function()
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('with errors', function()
|
||||
it('transforms API error from nvim_buf_set_lines into lua error', function()
|
||||
fn.setline(1, { 'abc', 'def', 'a\nb', 'ttt' })
|
||||
@@ -65,7 +66,7 @@ describe('luaeval(vim.api.…)', function()
|
||||
)
|
||||
end)
|
||||
|
||||
it('correctly evaluates API code which calls luaeval', function()
|
||||
it('evaluates API code which calls luaeval', function()
|
||||
local str = (
|
||||
([===[vim.api.nvim_eval([==[
|
||||
luaeval('vim.api.nvim_eval([=[
|
||||
@@ -78,7 +79,7 @@ describe('luaeval(vim.api.…)', function()
|
||||
eq(1, fn.luaeval(str))
|
||||
end)
|
||||
|
||||
it('correctly converts from API objects', function()
|
||||
it('converts from API objects', function()
|
||||
eq(1, fn.luaeval('vim.api.nvim_eval("1")'))
|
||||
eq('1', fn.luaeval([[vim.api.nvim_eval('"1"')]]))
|
||||
eq('Blobby', fn.luaeval('vim.api.nvim_eval("0z426c6f626279")'))
|
||||
@@ -110,7 +111,7 @@ describe('luaeval(vim.api.…)', function()
|
||||
eq({ { 42 }, {} }, fn.luaeval([=[vim.api.nvim_eval('[[42], []]')]=]))
|
||||
end)
|
||||
|
||||
it('correctly converts to API objects', function()
|
||||
it('converts to API objects', function()
|
||||
eq(1, fn.luaeval('vim.api.nvim__id(1)'))
|
||||
eq('1', fn.luaeval('vim.api.nvim__id("1")'))
|
||||
eq({ 1 }, fn.luaeval('vim.api.nvim__id({1})'))
|
||||
@@ -145,6 +146,20 @@ describe('luaeval(vim.api.…)', function()
|
||||
fn.luaeval('vim.api.nvim__id({foo=1, bar={42, {{baz=true}, 5}}})')
|
||||
)
|
||||
|
||||
-- Drops a metatable (e.g. `__index`, `__tostring`): converts only the table's own fields.
|
||||
eq(
|
||||
{ { foo = 1 }, true },
|
||||
exec_lua(function()
|
||||
local tbl = setmetatable({ foo = 1 }, {
|
||||
__index = { bar = 2 },
|
||||
__tostring = function()
|
||||
return 'x'
|
||||
end,
|
||||
})
|
||||
return { tbl, getmetatable(vim.api.nvim__id(tbl)) == nil }
|
||||
end)
|
||||
)
|
||||
|
||||
eq(true, fn.luaeval('vim.api.nvim__id(vim.api.nvim__id)(true)'))
|
||||
eq(
|
||||
42,
|
||||
@@ -155,7 +170,7 @@ describe('luaeval(vim.api.…)', function()
|
||||
)
|
||||
end)
|
||||
|
||||
it('correctly converts container objects with type_idx to API objects', function()
|
||||
it('converts container objects with type_idx to API objects', function()
|
||||
eq(
|
||||
5,
|
||||
eval('type(luaeval("vim.api.nvim__id({[vim.type_idx]=vim.types.float, [vim.val_idx]=0})"))')
|
||||
@@ -192,7 +207,7 @@ describe('luaeval(vim.api.…)', function()
|
||||
)
|
||||
end)
|
||||
|
||||
it('correctly converts arrays with type_idx to API objects', function()
|
||||
it('converts arrays with type_idx to API objects', function()
|
||||
eq(3, eval([[type(luaeval('vim.api.nvim__id_array({[vim.type_idx]=vim.types.array})'))]]))
|
||||
|
||||
eq({}, fn.luaeval('vim.api.nvim__id_array({[vim.type_idx]=vim.types.array})'))
|
||||
@@ -226,7 +241,7 @@ describe('luaeval(vim.api.…)', function()
|
||||
eq(3, eval([[type(luaeval('vim.api.nvim__id_array({})'))]]))
|
||||
end)
|
||||
|
||||
it('correctly converts dictionaries with type_idx to API objects', function()
|
||||
it('converts dictionaries with type_idx to API objects', function()
|
||||
eq(4, eval([[type(luaeval('vim.api.nvim__id_dict({[vim.type_idx]=vim.types.dictionary})'))]]))
|
||||
|
||||
eq({}, fn.luaeval('vim.api.nvim__id_dict({[vim.type_idx]=vim.types.dictionary})'))
|
||||
|
||||
@@ -207,21 +207,21 @@ describe('vim.net._ssh', function()
|
||||
table.insert(progress, ('%s: %s'):format(ev.data.status, table.concat(ev.data.text)))
|
||||
end,
|
||||
})
|
||||
local sock, teardown = require('vim.net._ssh').connect('user@test-server')
|
||||
local sock, teardown = require('vim.net._ssh').connect('ssh://user@test-server')
|
||||
teardown()
|
||||
return { sock, logfile, progress }
|
||||
]]))
|
||||
|
||||
assert(sock:match('_remote_nvim%.sock$'))
|
||||
eq({
|
||||
'running: Connecting to user@test-server...',
|
||||
'running: Connecting to ssh://user@test-server...',
|
||||
'running: Starting remote Nvim...',
|
||||
'success: Connected to user@test-server',
|
||||
'success: Connected to ssh://user@test-server',
|
||||
}, progress)
|
||||
local msg = 'This will install Nvim on user@test-server (in ~/.local/share/nvim-remote/), '
|
||||
local msg = 'This will install Nvim on ssh://user@test-server (in ~/.local/share/nvim-remote/), '
|
||||
.. 'if missing.\n'
|
||||
eq({ { msg, 'Connect? &Yes\n&No', 1, 'Question' } }, n.exec_lua('return _G.confirm_log'))
|
||||
t.matches('Connected to user@test%-server', t.read_file(logfile))
|
||||
t.matches('Connected to ssh://user@test%-server', t.read_file(logfile))
|
||||
local tunnel_args = t.read_file(fake_dir .. '/tunnel.args')
|
||||
t.matches('BatchMode=yes', tunnel_args)
|
||||
t.matches('RemoteCommand=none', tunnel_args)
|
||||
@@ -233,13 +233,14 @@ describe('vim.net._ssh', function()
|
||||
n.poke_eventloop()
|
||||
n.assert_alive()
|
||||
|
||||
-- Explicit ssh_config values are kept.
|
||||
-- Explicit ssh_config values are kept. The target is decoded, with an IPv6 host unbracketed.
|
||||
setup_fake_ssh({ ssh_config = 'stricthostkeychecking yes\nserveraliveinterval 60\n' })
|
||||
n.exec_lua(function()
|
||||
local _, teardown = require('vim.net._ssh').connect('user@test-server')
|
||||
local _, teardown = require('vim.net._ssh').connect('ssh://a%40b@[::1]:2222/x')
|
||||
teardown()
|
||||
end)
|
||||
local explicit_args = t.read_file(fake_dir .. '/tunnel.args')
|
||||
t.matches(' %-p 2222 %-%- a@b@::1 sh ', explicit_args)
|
||||
eq(nil, explicit_args:find('StrictHostKeyChecking', 1, true))
|
||||
eq(nil, explicit_args:find('ServerAliveInterval', 1, true))
|
||||
|
||||
@@ -247,7 +248,7 @@ describe('vim.net._ssh', function()
|
||||
setup_fake_ssh()
|
||||
t.matches(
|
||||
'UI detached: channel 999',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('user@test-server', 999)]])
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh://user@test-server', 999)]])
|
||||
)
|
||||
eq(true, tunnel_exited())
|
||||
eq(true, vim.uv.fs_stat(fake_dir .. '/forward.cancelled') ~= nil)
|
||||
@@ -261,7 +262,7 @@ describe('vim.net._ssh', function()
|
||||
os.remove(logfile)
|
||||
-- Connects, then ends the session as the remote does: its Nvim quits.
|
||||
local session = [[
|
||||
local sock = require('vim.net._ssh').connect('user@test-server')
|
||||
local sock = require('vim.net._ssh').connect('ssh://user@test-server')
|
||||
vim.rpcnotify(vim.fn.sockconnect('pipe', sock, { rpc = true }), 'nvim_command', 'qall!')
|
||||
]]
|
||||
|
||||
@@ -280,7 +281,7 @@ describe('vim.net._ssh', function()
|
||||
skip(is_os('win'), 'remote-ssh is POSIX-only')
|
||||
setup_fake_ssh()
|
||||
local base, prerelease = unpack(n.exec_lua(function()
|
||||
local _, teardown = require('vim.net._ssh').connect('user@test-server')
|
||||
local _, teardown = require('vim.net._ssh').connect('ssh://user@test-server')
|
||||
teardown()
|
||||
local v = vim.version()
|
||||
return { ('v%d.%d.%d'):format(v.major, v.minor, v.patch), v.prerelease and true or false }
|
||||
@@ -371,9 +372,12 @@ describe('vim.net._ssh', function()
|
||||
end)
|
||||
t.matches(
|
||||
'SSH tunnel exited before the remote Nvim was ready: channel 0: open failed',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('user@test-server')]])
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh://user@test-server')]])
|
||||
)
|
||||
eq(
|
||||
'failed: Failed to connect to ssh://user@test-server',
|
||||
n.exec_lua('return _G.last_progress')
|
||||
)
|
||||
eq('failed: Failed to connect to user@test-server', n.exec_lua('return _G.last_progress'))
|
||||
eq(true, vim.uv.fs_stat(fake_dir .. '/forward.cancelled') ~= nil)
|
||||
end)
|
||||
|
||||
@@ -383,7 +387,11 @@ describe('vim.net._ssh', function()
|
||||
})
|
||||
t.matches(
|
||||
'Remote Nvim crashed during startup',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('user@test-server')]])
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh://user@test-server')]])
|
||||
)
|
||||
eq(
|
||||
'No host in SSH URI: ssh:///x',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh:///x')]])
|
||||
)
|
||||
|
||||
-- The script runs, but sshd refused the forward.
|
||||
@@ -392,7 +400,7 @@ describe('vim.net._ssh', function()
|
||||
})
|
||||
t.matches(
|
||||
'Remote Nvim is not reachable through the SSH forward',
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('user@test-server')]])
|
||||
t.pcall_err(n.exec_lua, [[require('vim.net._ssh').connect('ssh://user@test-server')]])
|
||||
)
|
||||
eq(true, vim.uv.fs_stat(fake_dir .. '/forward.cancelled') ~= nil)
|
||||
end)
|
||||
@@ -402,11 +410,10 @@ describe('vim.net._ssh', function()
|
||||
setup_fake_ssh()
|
||||
local function remote_dir()
|
||||
n.exec_lua([[
|
||||
local _, teardown = require('vim.net._ssh').connect('user@test-server')
|
||||
local _, teardown = require('vim.net._ssh').connect('ssh://user@test-server')
|
||||
teardown()
|
||||
]])
|
||||
local args = t.read_file(fake_dir .. '/tunnel.args')
|
||||
t.matches('mkdir %-m 700 "%$dir"', args)
|
||||
return args:match(':(/tmp/nvim%-remote%.%x+)/nvim%.sock ')
|
||||
end
|
||||
|
||||
|
||||
@@ -59,8 +59,15 @@ describe('vim.uri', function()
|
||||
end)
|
||||
|
||||
it('roundtrip to_fname(from_fname(path))', function()
|
||||
local paths =
|
||||
{ '/a/c#/b?c', '/a/%25/%A0.txt', '/a b/#@file@#', 'C:/a b/c#', '//server/share/a b' }
|
||||
local paths = {
|
||||
'/a/c#/b?c',
|
||||
'/a/%25/%A0.txt',
|
||||
'/a b/#@file@#',
|
||||
'C:/a b/c#',
|
||||
'//server/share/a b',
|
||||
-- A URI passes through both.
|
||||
'jdt://a/b?c',
|
||||
}
|
||||
eq(
|
||||
paths,
|
||||
exec_lua(function()
|
||||
@@ -82,7 +89,7 @@ describe('vim.uri', function()
|
||||
exec_lua("uri = 'file:/Foo/Bar/Baz.txt'")
|
||||
eq('/Foo/Bar/Baz.txt', exec_lua('return vim.uri.to_fname(uri)'))
|
||||
eq(
|
||||
{ '/Foo/Bar', '/Foo/Bar', '/Foo/Bar', '/', '/Foo' },
|
||||
{ '/Foo/Bar', '/Foo/Bar', '/Foo/Bar', '/', '/Foo', '/Foo', 'jdt://a/b' },
|
||||
exec_lua(function()
|
||||
local uris = {
|
||||
'file:Foo/Bar',
|
||||
@@ -90,6 +97,8 @@ describe('vim.uri', function()
|
||||
'file://localhost/Foo/Bar',
|
||||
'file://',
|
||||
'FILE:///Foo',
|
||||
vim.uri.parse('file:///Foo'),
|
||||
vim.uri.parse('jdt://a/b'),
|
||||
}
|
||||
return vim.tbl_map(vim.uri.to_fname, uris)
|
||||
end)
|
||||
@@ -208,6 +217,9 @@ describe('vim.uri', function()
|
||||
return pcall(vim.uri.to_fname, 'not_an_uri.txt')
|
||||
]]
|
||||
)
|
||||
-- Invalid authority.
|
||||
eq(false, exec_lua([[return pcall(vim.uri.to_fname, 'https://h:x')]]))
|
||||
eq(false, exec_lua([[return pcall(vim.uri.to_fname, 'file://h:x/a')]]))
|
||||
end)
|
||||
|
||||
it('does not treat comma as a scheme character', function()
|
||||
@@ -323,97 +335,198 @@ describe('vim.uri', function()
|
||||
it('parse()', function()
|
||||
local uris = {
|
||||
'https://u@example.com:80/a%20b?q=1#f?#',
|
||||
'ssh://a%40b:p%3Aw@[::1]:22',
|
||||
'ssh://HOST:/',
|
||||
'file:///a',
|
||||
'deno:/https/a',
|
||||
'mailto:a@example.com',
|
||||
'MailTo:a@example.com',
|
||||
'inmemory:',
|
||||
'foo:api/files',
|
||||
'f3ile:#d',
|
||||
'file:////shares/p',
|
||||
'file://C:/a',
|
||||
'stuff:?',
|
||||
'jdt://contents/A.class?=a/%5C/b+c',
|
||||
'http://[fe80::1%25en0]:8080/',
|
||||
'C:/a',
|
||||
'/a',
|
||||
-- Invalid.
|
||||
'https://h:x',
|
||||
'http://::1/',
|
||||
'http://[::1/',
|
||||
'http://[zz]/',
|
||||
'http://h:65536/',
|
||||
'http://a b/',
|
||||
'http://a%2@h/',
|
||||
}
|
||||
local parsed = exec_lua(function()
|
||||
-- Roundtrip: tostring() gives the original URI, or its normalized form.
|
||||
local normal = {
|
||||
['ssh://HOST:/'] = 'ssh://host/',
|
||||
['MailTo:a@example.com'] = 'mailto:a@example.com',
|
||||
['file://C:/a'] = 'file:///C:/a',
|
||||
}
|
||||
return vim.tbl_map(function(uri)
|
||||
local u = vim.uri.parse(uri)
|
||||
-- Roundtrip: tostring() gives the original URI.
|
||||
assert(not u or tostring(u) == uri, uri)
|
||||
assert(not u or tostring(u) == (normal[uri] or uri), ('%s => %s'):format(uri, tostring(u)))
|
||||
return u or {}
|
||||
end, uris)
|
||||
end)
|
||||
eq({
|
||||
{
|
||||
scheme = 'https',
|
||||
authority = 'u@example.com:80',
|
||||
user = 'u',
|
||||
host = 'example.com',
|
||||
port = 80,
|
||||
path = '/a%20b',
|
||||
query = 'q=1',
|
||||
query = { q = '1' },
|
||||
querystr = 'q=1',
|
||||
fragment = 'f?#',
|
||||
},
|
||||
{ scheme = 'file', authority = '', path = '/a' },
|
||||
{
|
||||
scheme = 'ssh',
|
||||
user = 'a@b',
|
||||
password = 'p:w',
|
||||
host = '::1',
|
||||
port = 22,
|
||||
path = '',
|
||||
},
|
||||
{ scheme = 'ssh', host = 'host', path = '/' },
|
||||
{ scheme = 'file', host = '', path = '/a' },
|
||||
{ scheme = 'deno', path = '/https/a' },
|
||||
{ scheme = 'mailto', path = 'a@example.com' },
|
||||
{ scheme = 'inmemory', path = '' },
|
||||
{ scheme = 'foo', path = 'api/files' },
|
||||
{ scheme = 'f3ile', path = '', fragment = 'd' },
|
||||
{ scheme = 'file', authority = '', path = '//shares/p' },
|
||||
{ scheme = 'stuff', path = '', query = '' },
|
||||
{ scheme = 'file', host = '', path = '//shares/p' },
|
||||
{ scheme = 'file', host = '', path = '/C:/a' },
|
||||
{ scheme = 'stuff', path = '', query = {}, querystr = '' },
|
||||
{
|
||||
scheme = 'jdt',
|
||||
host = 'contents',
|
||||
path = '/A.class',
|
||||
query = { [''] = 'a/\\/b c' },
|
||||
querystr = '=a/%5C/b+c',
|
||||
},
|
||||
{ scheme = 'http', host = 'fe80::1%en0', port = 8080, path = '/' },
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
}, parsed)
|
||||
end)
|
||||
|
||||
describe('SSH URI parser', function()
|
||||
it('parses ssh://user@host:port', function()
|
||||
local uri = exec_lua(function()
|
||||
return require('vim.uri')._parse_ssh_uri('ssh://root@localhost:2222')
|
||||
it('normalize()', function()
|
||||
eq(
|
||||
{
|
||||
'https://User@example.com/a/c/~%2F?q=~%7B#~',
|
||||
'http://h/',
|
||||
'mailto:a/../b',
|
||||
'file:///b',
|
||||
true,
|
||||
false,
|
||||
},
|
||||
exec_lua(function()
|
||||
return {
|
||||
tostring(
|
||||
vim.uri.normalize('HTTPS://User@Example.COM:443/a/./b/../c/%7e%2f?q=%7e%7b#%7e')
|
||||
),
|
||||
tostring(vim.uri.normalize('http://h:80')),
|
||||
tostring(vim.uri.normalize('mailto:a/../b')),
|
||||
tostring(vim.uri.normalize('file://LocalHost/a/../b')),
|
||||
vim.uri.normalize('HTTP://h/%7E') == vim.uri.normalize(vim.uri.parse('http://h:80/~')),
|
||||
(pcall(vim.uri.normalize, 'a/b')),
|
||||
}
|
||||
end)
|
||||
eq('root', uri.user)
|
||||
eq('localhost', uri.host)
|
||||
eq('2222', uri.port)
|
||||
)
|
||||
end)
|
||||
|
||||
eq(
|
||||
{ user = 'u', host = '::1', port = '22' },
|
||||
exec_lua(function()
|
||||
return require('vim.uri')._parse_ssh_uri('ssh://u@[::1]:22/home/u')
|
||||
end)
|
||||
)
|
||||
eq(
|
||||
{ host = 'host' },
|
||||
exec_lua(function()
|
||||
return require('vim.uri')._parse_ssh_uri('ssh://host/')
|
||||
end)
|
||||
)
|
||||
eq(
|
||||
{ user = 'first.last@corp', host = 'host' },
|
||||
exec_lua(function()
|
||||
return require('vim.uri')._parse_ssh_uri('ssh://first.last%40corp@host')
|
||||
end)
|
||||
)
|
||||
eq(
|
||||
'No host in SSH URI: ssh:///x',
|
||||
t.pcall_err(exec_lua, function()
|
||||
return require('vim.uri')._parse_ssh_uri('ssh:///x')
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
it('parses user@host', function()
|
||||
local uri = exec_lua(function()
|
||||
return require('vim.uri')._parse_ssh_uri('admin@server.local')
|
||||
it('join()', function()
|
||||
eq(
|
||||
{
|
||||
'file:///home/u/proj/src/a%231.lua',
|
||||
'file:///home/u/proj/src/',
|
||||
'file:///C:/x',
|
||||
'file:///x',
|
||||
'https://h/api/users/a%20b?v=1',
|
||||
'https://h/a',
|
||||
{ 'file:///a/b', 'file:///a' },
|
||||
{ false, 'file:///a/ab:c' },
|
||||
false,
|
||||
},
|
||||
exec_lua(function()
|
||||
local base = assert(vim.uri.parse('file:///a'))
|
||||
return {
|
||||
tostring(vim.uri.join('file:///home/u/proj', 'src', 'a#1.lua')),
|
||||
tostring(vim.uri.join('file:///home/u/proj/', 'src/')),
|
||||
-- Not above the drive letter, or the root.
|
||||
tostring(vim.uri.join('file:///C:/a', '../../x')),
|
||||
tostring(vim.uri.join('file:///a', '../../x')),
|
||||
tostring(vim.uri.join('https://h/api/?v=1', 'users', 'a b')),
|
||||
tostring(vim.uri.join('https://h', 'a')),
|
||||
-- Does not modify a `vim.Uri` base.
|
||||
{ tostring(vim.uri.join(base, 'b')), tostring(base) },
|
||||
-- A segment can't be a URI, but can contain ":".
|
||||
{
|
||||
(pcall(vim.uri.join, 'file:///a', 'file:///b')),
|
||||
tostring(vim.uri.join('file:///a', 'ab:c')),
|
||||
},
|
||||
(pcall(vim.uri.join, 'a/b', 'c')),
|
||||
}
|
||||
end)
|
||||
eq('admin', uri.user)
|
||||
eq('server.local', uri.host)
|
||||
eq(nil, uri.port)
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
it('parses host only', function()
|
||||
local uri = exec_lua(function()
|
||||
return require('vim.uri')._parse_ssh_uri('myalias')
|
||||
it('encode_query(), decode_query()', function()
|
||||
eq(
|
||||
{
|
||||
'n=1&q=a+b%26c%3Dd&tag=x&tag=y',
|
||||
{ q = 'a b&c=d', tag = { 'x', 'y' }, n = '1', flag = '' },
|
||||
},
|
||||
exec_lua(function()
|
||||
local query = vim.uri.encode_query({ q = 'a b&c=d', tag = { 'x', 'y' }, n = 1 })
|
||||
return { query, vim.uri.decode_query(('%s&flag'):format(query)) }
|
||||
end)
|
||||
eq(nil, uri.user)
|
||||
eq('myalias', uri.host)
|
||||
eq(nil, uri.port)
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
it('new(), tostring()', function()
|
||||
-- tostring() builds the URI from the fields.
|
||||
eq(
|
||||
{
|
||||
'https://a%40b:p%3Aw@[::1]:8080/a',
|
||||
true,
|
||||
'https://[::1]:8080/a',
|
||||
'https://h/s?n=1&q=a+b',
|
||||
'https://h/?page=2&q=1',
|
||||
'x:',
|
||||
false,
|
||||
},
|
||||
exec_lua(function()
|
||||
local u = assert(vim.uri.parse('https://example.com/a'))
|
||||
u.user, u.password, u.host, u.port = 'a@b', 'p:w', '::1', 8080
|
||||
return {
|
||||
tostring(u),
|
||||
vim.uri.parse('HTTP://h/a') == vim.uri.new({ scheme = 'http', host = 'h', path = '/a' }),
|
||||
tostring(vim.uri.new({ scheme = 'HTTPS', host = '::1', port = 8080, path = '/a' })),
|
||||
tostring(
|
||||
vim.uri.new({ scheme = 'https', host = 'h', path = '/s', query = { q = 'a b', n = 1 } })
|
||||
),
|
||||
(function()
|
||||
-- `querystr` takes precedence over `query`.
|
||||
local q = assert(vim.uri.parse('https://h/?q=1'))
|
||||
q.query.page, q.querystr = '2', nil
|
||||
return tostring(q)
|
||||
end)(),
|
||||
tostring(vim.uri.new({ scheme = 'x' })),
|
||||
(pcall(vim.uri.new, {})),
|
||||
}
|
||||
end)
|
||||
)
|
||||
end)
|
||||
end)
|
||||
@@ -34,15 +34,31 @@ describe(':checkhealth', function()
|
||||
eq("Invalid 'runtimepath'", string.match(err, 'Invalid.*'))
|
||||
end)
|
||||
|
||||
it('detects invalid $VIM', function()
|
||||
clear()
|
||||
it('vim.health report', function()
|
||||
-- Invalid $TERM.
|
||||
clear({ env = { TERM = 'a&b+c' } })
|
||||
exec_lua([[require('vim.health.health')._skip_update_check = true]])
|
||||
-- Do this after startup, otherwise it just breaks $VIMRUNTIME.
|
||||
-- Invalid $VIM. Do this after startup, otherwise it just breaks $VIMRUNTIME.
|
||||
command("let $VIM='zub'")
|
||||
command('checkhealth vim.health')
|
||||
retry(nil, 10000, function()
|
||||
eq('function', exec_lua('return type(_G.nvim_health_bugreport_open)'))
|
||||
matches('ERROR $VIM .* zub', curbuf_contents())
|
||||
end)
|
||||
-- All sections ran.
|
||||
eq(nil, curbuf_contents():match('Failed to run healthcheck.*'))
|
||||
|
||||
-- Bug-report URL-encodes the report.
|
||||
local query = exec_lua(function()
|
||||
local url --- @type string
|
||||
vim.ui.open = function(u)
|
||||
url = u
|
||||
end
|
||||
_G.nvim_health_bugreport_open()
|
||||
return assert(vim.uri.parse(url)).query
|
||||
end)
|
||||
eq('Bug', query.type)
|
||||
matches('%$TERM environment variable: `a&b%+c`', query.body)
|
||||
end)
|
||||
|
||||
it('getcompletion()', function()
|
||||
|
||||
Reference in new issue
Block a user