feat(lua): vim.uri #24527

Problem:
- `vim.fs.abspath('fugitive:///a')` returns "<CWD>/fugitive:/a", so
  filetype functions get a mangled path for URI buffers.
- `vim.fs.root()` doesn't accept "file://" URIs.
- No URI parser, so modules use ad-hoc scheme patterns, e.g.
  `vim.ui.open('~/a:b')` treats the path as a URL.

Solution:
- Introduce `vim.uri.parse()`, and use it everywhere.
  - DWIM: a one-letter "scheme" is a drive letter.
- `vim.fs.abspath()` returns URLs as-is.
- Expose `vim.uri` a module. Deprecate the old `vim.uri_xx()` names.
This commit is contained in:
Justin M. Keyes authored and GitHub committed 2026-11-01 13:20:08 -05:00
1 parent 48dcaab903
commit 5a37eb7a49
50 files changed
+841 -639

No files matched your search

+9 -3
View File
@@ -33,10 +33,16 @@ HIGHLIGHTS
LUA
• vim.F.if_nil() Renamed to |vim.nonnil()|
• vim.F.if_nil() Renamed to |vim.nonnil()|
• vim.F.ok_or_nil()
• vim.F.npcall() Renamed to |vim.npcall()|
• vim.F.nil_wrap() Use |vim.npcall()| instead
• vim.F.npcall() Renamed to |vim.npcall()|
• vim.F.nil_wrap() Use |vim.npcall()| instead
• *vim.uri_decode()* Renamed to |vim.uri.decode()|
• *vim.uri_encode()* Renamed to |vim.uri.encode()|
• *vim.uri_from_bufnr()* Renamed to |vim.uri.from_buf()|
• *vim.uri_from_fname()* Renamed to |vim.uri.from_fname()|
• *vim.uri_to_bufnr()* Renamed to |vim.uri.to_buf()|
• *vim.uri_to_fname()* Renamed to |vim.uri.to_fname()|
TREESITTER
+87 -18
View File
@@ -2691,7 +2691,8 @@ vim.fs.abspath({path}, {opts}) *vim.fs.abspath()*
the path (unless plain=true). Does not check if the path exists, normalize
the path, resolve symlinks or hardlinks (including "." and ".."), or
expand environment variables. If the path is already absolute, it is
returned unchanged. Converts `\` path separators to `/`.
returned unchanged. Converts `\` path separators to `/`. URLs (e.g.
"fugitive:///a") are returned as-is.
Attributes: ~
Since: 0.11.0
@@ -3030,9 +3031,9 @@ vim.fs.root({source}, {marker}) *vim.fs.root()*
Since: 0.10.0
Parameters: ~
• {source} (`integer|string`) Buffer number (0 for current buffer) or
file path (absolute or relative, expanded via `abspath()`)
to begin the search from.
• {source} (`integer|string`) Buffer number (0 for current buffer),
file path (absolute or relative, expanded via `abspath()`),
or "file://" URI to begin the search from.
• {marker} (`(string|string[]|fun(name: string, path: string): boolean)[]|string|fun(name: string, path: string): boolean`)
Filename, function, or list thereof, that decides how to
find the root. To indicate "equal priority", specify items
@@ -5868,8 +5869,24 @@ vim.ui.img.set({data_or_id}, {opts}) *vim.ui.img.set()*
==============================================================================
Lua module: vim.uri *vim.uri*
vim.uri_decode({str}) *vim.uri_decode()*
URI-decodes a string containing percent escapes.
vim.Uri *vim.Uri*
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".
vim.uri.decode({str}) *vim.uri.decode()*
Decodes a URI-encoded string.
Attributes: ~
Since: 0.13.0
Parameters: ~
• {str} (`string`) string to decode
@@ -5877,8 +5894,11 @@ vim.uri_decode({str}) *vim.uri_decode()*
Return: ~
(`string`) decoded string
vim.uri_encode({str}, {rfc}) *vim.uri_encode()*
URI-encodes a string using percent escapes.
vim.uri.encode({str}, {rfc}) *vim.uri.encode()*
URI-encodes a string.
Attributes: ~
Since: 0.13.0
Parameters: ~
• {str} (`string`) string to encode
@@ -5887,8 +5907,11 @@ vim.uri_encode({str}, {rfc}) *vim.uri_encode()*
Return: ~
(`string`) encoded string
vim.uri_from_bufnr({buf}) *vim.uri_from_bufnr()*
Gets a URI from a bufnr.
vim.uri.from_buf({buf}) *vim.uri.from_buf()*
Gets a URI from a buffer.
Attributes: ~
Since: 0.13.0
Parameters: ~
• {buf} (`integer`)
@@ -5896,8 +5919,19 @@ vim.uri_from_bufnr({buf}) *vim.uri_from_bufnr()*
Return: ~
(`string`) URI
vim.uri_from_fname({path}) *vim.uri_from_fname()*
Gets a URI from a file path.
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).
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")
<
Attributes: ~
Since: 0.13.0
Parameters: ~
• {path} (`string`) Path to file
@@ -5905,18 +5939,53 @@ vim.uri_from_fname({path}) *vim.uri_from_fname()*
Return: ~
(`string`) URI
vim.uri_to_bufnr({uri}) *vim.uri_to_bufnr()*
Gets the buffer for a uri. Creates a new unloaded buffer if no buffer for
the uri already exists.
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.
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'
<
Attributes: ~
Since: 0.13.0
Parameters: ~
• {uri} (`string`)
Return: ~
(`integer`) bufnr
(`vim.Uri?`) URI components, or nil if `uri` has no scheme.
vim.uri_to_fname({uri}) *vim.uri_to_fname()*
Gets a filename from a URI.
vim.uri.to_buf({uri}) *vim.uri.to_buf()*
Gets the buffer for a uri. Creates a new unloaded buffer if no buffer for
the uri already exists.
Attributes: ~
Since: 0.13.0
Parameters: ~
• {uri} (`string`)
Return: ~
(`integer`) buf
vim.uri.to_fname({uri}) *vim.uri.to_fname()*
Gets a filename from a URI. Drops the query and fragment, so "?" and "#"
in a filename must be percent-encoded.
Example: >lua
vim.uri.to_fname('file:///a%3Fb?c#d') --> '/a?b'
vim.uri.to_fname('file:///C:/a/b') --> 'C:/a/b'
vim.uri.to_fname('file://server/share/a') --> '//server/share/a'
vim.uri.to_fname('jdt://a/b') --> 'jdt://a/b'
<
Attributes: ~
Since: 0.13.0
Parameters: ~
• {uri} (`string`)
+11
View File
@@ -45,6 +45,11 @@ LUA
`{ key = value }` instead of an array of `"key:value"` strings.
- The `,,` literal-comma convention (e.g. 'isfname') is not reconstructed in
the structured view; the raw string value is unchanged.
• Backslashes are not supported in URIs/URLs. E.g. "http:\\host" is treated as
a filepath, not a URL, in buffer names, |gf|, etc.
• |vim.uri.to_fname()| returns "/" separators (also on Windows), drops the URI
query, and converts a URI host to a UNC path ("file://server/share" =>
"//server/share").
DIAGNOSTICS
@@ -438,6 +443,8 @@ LUA
• |vim.pos.cursor()| without arguments returns the cursor position of the
current window.
• |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()|.
• |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.
@@ -600,6 +607,10 @@ These existing features changed their behavior.
`opts.plain=true` and now does not expand leading tildes ("~") in addition
to environment variables ("expand_env" is still accepted, for backwards
compatibility).
• |vim.fs.abspath()| returns URLs (e.g. "fugitive:///a") as-is, so
|vim.filetype.add()| functions receive URI buffer names unchanged.
• |vim.uri.from_fname()| resolves relative paths to absolute paths, and
converts UNC paths.
• |:helptags| finds help tags with the "vimdoc" |treesitter| parser (and thus
requires it to be installed).
• `:helptags ALL` reports |E152| for "doc" directories it cannot write,
+1 -1
View File
@@ -156,7 +156,7 @@ local function urls()
vim.api.nvim_buf_set_extmark(0, url_ns, start_line, start_col, {
end_line = end_line,
end_col = end_col,
url = base .. vim.uri_encode(tag),
url = base .. vim.uri.encode(tag),
})
end
end
+2 -1
View File
@@ -2,6 +2,7 @@
local api = vim.api
local fs = vim.fs
local is_url = vim._core.path_is_url
local M = {}
@@ -48,7 +49,7 @@ end
---@param path string
function M.open_parent_path(path)
if path == '' or path:find('^%a[%w+.-]*://') then
if path == '' or is_url(path) then
navigate('.')
return
end
+2 -1
View File
@@ -1,6 +1,7 @@
local api = vim.api
local uv = vim.uv
local notify = require('vim._core.util').notify
local is_url = vim._core.path_is_url
local M = {}
@@ -530,7 +531,7 @@ function M.open_parent(buf, name)
require('nvim.dir').open(buf, name, M, { name = child, dir = true })
return
end
if name:match('^%a[%w+.-]*://') then
if is_url(name) then
return
end
require('nvim.dir.fs').open_parent_path(state.source)
+1 -5
View File
@@ -31,13 +31,9 @@ for k, v in pairs({
end
-- There are things which have special rules in vim._init_packages
-- for legacy reasons (uri) or for performance (_inspector).
-- for performance (_inspector).
-- most new things should go into a submodule namespace ( vim.foobar.do_thing() )
vim._extra = {
uri_from_fname = true,
uri_from_bufnr = true,
uri_to_fname = true,
uri_to_bufnr = true,
show_pos = true,
inspect_pos = true,
}
+23 -10
View File
@@ -53,15 +53,26 @@ end
require('vim._core.shared')
vim._submodules = {
inspect = true,
version = true,
fs = true,
glob = true,
inspect = true,
iter = true,
provider = true,
re = true,
text = true,
tty = true,
provider = true,
uri = true,
version = true,
}
--- Deprecated aliases of `vim.uri` functions.
local uri_aliases = {
uri_decode = 'decode',
uri_encode = 'encode',
uri_from_bufnr = 'from_buf',
uri_from_fname = 'from_fname',
uri_to_bufnr = 'to_buf',
uri_to_fname = 'to_fname',
}
-- These are for loading runtime modules in the vim namespace lazily.
@@ -74,14 +85,16 @@ setmetatable(vim, {
elseif key == 'inspect_pos' or key == 'show_pos' then
require('vim._inspector')
return t[key]
elseif vim.startswith(key, 'uri_') then
--- @type any?
local val = require('vim.uri')[key]
if val ~= nil then
-- Expose all `vim.uri` functions on the `vim` module.
t[key] = val
return t[key]
elseif uri_aliases[key] then
local name = uri_aliases[key]
local fn = require('vim.uri')[name] --- @type function
t[key] = function(...)
if not vim.is_thread() then
vim.deprecate(('vim.%s'):format(key), ('vim.uri.%s'):format(name), '0.15')
end
return fn(...)
end
return t[key]
end
end,
})
+13 -6
View File
@@ -37,13 +37,20 @@ vim.text = require('vim.text')
vim.treesitter = require('vim.treesitter')
vim.tty = require('vim.tty')
vim.ui = require('vim.ui')
vim.uri = require('vim.uri')
vim.version = require('vim.version')
local uri = require('vim.uri')
vim.uri_from_fname = uri.uri_from_fname
vim.uri_from_bufnr = uri.uri_from_bufnr
vim.uri_to_fname = uri.uri_to_fname
vim.uri_to_bufnr = uri.uri_to_bufnr
--- @deprecated
vim.uri_decode = vim.uri.decode
--- @deprecated
vim.uri_encode = vim.uri.encode
--- @deprecated
vim.uri_from_bufnr = vim.uri.from_buf
--- @deprecated
vim.uri_from_fname = vim.uri.from_fname
--- @deprecated
vim.uri_to_bufnr = vim.uri.to_buf
--- @deprecated
vim.uri_to_fname = vim.uri.to_fname
vim.provider = require('vim.provider')
+7
View File
@@ -197,6 +197,13 @@ function vim._core.loop_poll(timeout, fast_only) end
--- Flushes pending UI updates.
function vim._core.ui_flush() end
--- @nodoc
--- (C function: `path_with_url()`) Checks if `name` is a URL (not a file path): "deno:/a" and
--- "fugitive:///a" are URLs, "C:/a" and "mailto:a" are not.
--- @param name string
--- @return boolean
function vim._core.path_is_url(name) end
--- @nodoc
--- Checks for interrupt, clears it, and consumes input if present.
--- @return boolean
+1 -1
View File
@@ -218,7 +218,7 @@ function M.open(opts, ...)
-- range, plus description.
for _, info in ipairs(related_info) do
local location = info.location
local file_name = vim.fs.basename(vim.uri_to_fname(location.uri))
local file_name = vim.fs.basename(vim.uri.to_fname(location.uri))
local info_suffix = ': ' .. info.message
related_info_locations[#lines + 1] = location
lines[#lines + 1] = string.format(
+2 -1
View File
@@ -1,5 +1,6 @@
local api = vim.api
local fn = vim.fn
local is_url = vim._core.path_is_url
local M = {}
@@ -3286,7 +3287,7 @@ function M.match(args)
end
if name then
if name:sub(-1) == '/' and not name:find('^%a[%w+.-]*://') then
if name:sub(-1) == '/' and not is_url(name) then
return 'directory'
end
name = normalize_path(name)
+24 -4
View File
@@ -41,6 +41,14 @@
--- ```
local uv = vim.uv
local uri_parse = require('vim.uri').parse
--- @param path string
--- @return boolean
local function is_url(path)
-- XXX: always false in nlua0 (build-time generators), which has no C bindings.
return vim._core ~= nil and vim._core.path_is_url(path)
end
local M = {}
@@ -187,7 +195,8 @@ function M.slug(path, opts)
local maxlen = opts.maxlen or 180
-- Capture URI scheme before normalize() collapses "://" to ":/" (which looks like "C:/")
local uri_scheme = path:match('^([a-zA-Z][a-zA-Z0-9.+-]*)://') ---@type string?
local uri = uri_parse(path)
local uri_scheme = uri and uri.authority and uri.scheme
-- Normalize before computing the hash so equivalent paths produce the same result
path = vim.fs.normalize(path, { plain = true })
@@ -628,8 +637,8 @@ end
--- ```
---
--- @since 12
--- @param source integer|string Buffer number (0 for current buffer) or file path (absolute or
--- relative, expanded via `abspath()`) to begin the search from.
--- @param source integer|string Buffer number (0 for current buffer), file path (absolute or
--- relative, expanded via `abspath()`), or "file://" URI to begin the search from.
--- @param marker (string|string[]|fun(name: string, path: string): boolean)[]|string|fun(name: string, path: string): boolean
--- Filename, function, or list thereof, that decides how to find the root. To
--- indicate "equal priority", specify items in a nested list `{ { 'a.txt', 'b.lua' }, … }`.
@@ -654,6 +663,12 @@ function M.root(source, marker)
else
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
path = vim.uri.to_fname(path)
elseif is_url(path) then
path = assert(uv.cwd())
end
local markers = type(marker) == 'table' and marker or { marker }
for _, mark in ipairs(markers) do
@@ -1012,7 +1027,8 @@ end
--- Converts `path` to an absolute path. Expands tilde (~) at the beginning of the path (unless
--- plain=true). Does not check if the path exists, normalize the path, resolve symlinks or
--- hardlinks (including "." and ".."), or expand environment variables. If the path is already
--- absolute, it is returned unchanged. Converts `\` path separators to `/`.
--- absolute, it is returned unchanged. Converts `\` path separators to `/`. URLs (e.g.
--- "fugitive:///a") are returned as-is.
---
--- @since 13
--- @param path string Path
@@ -1027,6 +1043,10 @@ function M.abspath(path, opts)
vim.validate('cwd', opts.cwd, 'string', true)
vim.validate('plain', opts.plain, 'boolean', true)
if is_url(path) then
return path
end
-- Expand ~ to user's home directory
if not opts.plain then
path = expand_home(path)
+1 -1
View File
@@ -824,7 +824,7 @@ local function check_sysinfo()
if win == -1 then
return
end
local encoded_body = vim.uri_encode(body) --- @type string
local encoded_body = vim.uri.encode(body) --- @type string
local issue_url = 'https://github.com/neovim/neovim/issues/new?type=Bug&body=' .. encoded_body
--- Opens the prefilled issue from the checkhealth winbar.
+1 -1
View File
@@ -1,6 +1,6 @@
local fs = vim.fs -- "vim.fs" is a dependency, so must be loaded early.
local uv = vim.uv
local uri_encode = vim.uri_encode --- @type function
local uri_encode = vim.uri.encode --- @type function
--- @type (fun(modename: string): fun()|string)[]
local loaders = package.loaders
+2 -2
View File
@@ -60,7 +60,7 @@ function lsp._get_workspace_folders(workspace_folders)
elseif type(workspace_folders) == 'string' then
return {
{
uri = vim.uri_from_fname(workspace_folders),
uri = vim.uri.from_fname(workspace_folders),
name = workspace_folders,
},
}
@@ -916,7 +916,7 @@ local function buf_attach(bufnr)
end
attached_buffers[bufnr] = true
local uri = vim.uri_from_bufnr(bufnr)
local uri = vim.uri.from_buf(bufnr)
local augroup = ('nvim.lsp.b_%d_save'):format(bufnr)
local group = api.nvim_create_augroup(augroup, { clear = true })
nvim_on('BufWritePre', group, {
+3 -3
View File
@@ -182,7 +182,7 @@ function M._send_did_save(bufnr)
table.insert(entry.clients, client)
end
local uri = vim.uri_from_bufnr(bufnr)
local uri = vim.uri.from_buf(bufnr)
local text = vim.func._memoize('concat', vim.lsp._buf_get_full_text)
-- Send didOpen/didClose/didSave to all client groups.
@@ -197,7 +197,7 @@ function M._send_did_save(bufnr)
if old_name and name ~= old_name then
client:notify('textDocument/didClose', {
textDocument = {
uri = vim.uri_from_fname(old_name),
uri = vim.uri.from_fname(old_name),
},
}, bufnr)
client:notify('textDocument/didOpen', {
@@ -325,7 +325,7 @@ local function send_changes(bufnr, sync_kind, state, buf_state)
{ text = vim.lsp._buf_get_full_text(bufnr) },
}
end
local uri = vim.uri_from_bufnr(bufnr)
local uri = vim.uri.from_buf(bufnr)
for _, client in pairs(state.clients) do
if not client:is_stopped() and vim.lsp.buf_is_attached(bufnr, client.id) then
client:notify('textDocument/didChange', {
+3 -3
View File
@@ -69,11 +69,11 @@ local function watcher_rules(watchers, workspace_folders)
if type(pattern) == 'string' then
for _, folder in ipairs(workspace_folders) do
rules[#rules + 1] =
{ path = vim.uri_to_fname(folder.uri), pattern = pattern, events = events }
{ path = vim.uri.to_fname(folder.uri), pattern = pattern, events = events }
end
else
local uri = type(pattern.baseUri) == 'string' and pattern.baseUri or pattern.baseUri.uri
local base = vim.uri_to_fname(uri)
local base = vim.uri.to_fname(uri)
local prefix = vim.fs.normalize(base):gsub('/$', '')
rules[#rules + 1] = {
@@ -117,7 +117,7 @@ local function queue_change(client, fullpath, change_type)
local client_id = client.id
--- @type lsp.FileEvent
local change = {
uri = vim.uri_from_fname(fullpath),
uri = vim.uri.from_fname(fullpath),
type = to_lsp_change_type[change_type],
}
+5 -5
View File
@@ -551,7 +551,7 @@ function Client:initialize()
local workspace_folder = self.workspace_folders and self.workspace_folders[1]
if workspace_folder then
root_uri = workspace_folder.uri
root_path = vim.uri_to_fname(root_uri)
root_path = vim.uri.to_fname(root_uri)
end
-- HACK: Capability modules must be loaded
@@ -1106,8 +1106,8 @@ function Client:_get_registrations(provider, bufnr)
matched_regs[#matched_regs + 1] = reg
else
local language = self:_get_language_id(bufnr)
local uri = vim.uri_from_bufnr(bufnr)
local fname = vim.uri_to_fname(uri)
local uri = vim.uri.from_buf(bufnr)
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
@@ -1179,7 +1179,7 @@ function Client:_text_document_did_close_handler(bufnr)
if not self:supports_method('textDocument/didClose') then
return
end
local uri = vim.uri_from_bufnr(bufnr)
local uri = vim.uri.from_buf(bufnr)
local params = { textDocument = { uri = uri } }
self:notify('textDocument/didClose', params, bufnr)
end
@@ -1199,7 +1199,7 @@ function Client:_text_document_did_open_handler(bufnr)
self:notify('textDocument/didOpen', {
textDocument = {
version = lsp.util.buf_versions[bufnr],
uri = vim.uri_from_bufnr(bufnr),
uri = vim.uri.from_buf(bufnr),
languageId = self:_get_language_id(bufnr),
text = lsp._buf_get_full_text(bufnr),
},
+4 -4
View File
@@ -245,7 +245,7 @@ end
--- @param is_pull? boolean
--- @param pull_id? string
local function handle_diagnostics(uri, client_id, diagnostics, is_pull, pull_id)
local fname = vim.uri_to_fname(uri)
local fname = vim.uri.to_fname(uri)
if #diagnostics == 0 and vim.fn.bufexists(fname) == 0 then
return
@@ -327,7 +327,7 @@ function M.on_diagnostic(error, result, ctx)
-- Create a new client state if it doesn't exist for the related document. This will not enable
-- diagnostic pulling by itself, but will allow previous result IDs to be passed correctly the
-- next time this buffer's diagnostics are pulled.
local related_bufnr = vim.uri_to_bufnr(uri)
local related_bufnr = vim.uri.to_buf(uri)
local related_diagnostics = Diagnostics.active[related_bufnr] or Diagnostics:new(related_bufnr)
local related_state = related_diagnostics.client_state[client_id]
if not related_state then
@@ -470,7 +470,7 @@ local function previous_result_ids(client_id, identifier)
local result_id = state.result_id[key]
if result_id then
results[#results + 1] = {
uri = vim.uri_from_bufnr(bufnr),
uri = vim.uri.from_buf(bufnr),
value = result_id,
}
end
@@ -503,7 +503,7 @@ function M._workspace_diagnostics(opts)
---@type lsp.WorkspaceDiagnosticParams
local params = ctx.params
for _, report in ipairs(result.items) do
local bufnr = vim.uri_to_bufnr(report.uri)
local bufnr = vim.uri.to_buf(report.uri)
-- Start tracking the buffer (but don't send "textDocument/diagnostic" requests for it).
local provider = Diagnostics.active[bufnr] or Diagnostics:new(bufnr)
+1 -1
View File
@@ -319,7 +319,7 @@ function M.color_presentation()
return
end
local uri = vim.uri_from_bufnr(bufnr)
local uri = vim.uri.from_buf(bufnr)
local client = assert(lsp.get_client_by_id(client_id))
--- @type lsp.ColorPresentationParams
+3 -3
View File
@@ -315,7 +315,7 @@ RCS['textDocument/documentSymbol'] = response_to_list(
util.symbols_to_items,
'document symbols',
function(ctx)
local fname = vim.fn.fnamemodify(vim.uri_to_fname(ctx.params.textDocument.uri), ':.')
local fname = vim.fn.fnamemodify(vim.uri.to_fname(ctx.params.textDocument.uri), ':.')
return string.format('Symbols in %s', fname)
end
)
@@ -539,7 +539,7 @@ local function make_call_hierarchy_handler(direction)
local filename = nil
local bufnr = nil
if direction == 'from' then
filename = vim.uri_to_fname(call_hierarchy_item.uri)
filename = vim.uri.to_fname(call_hierarchy_item.uri)
else
bufnr = ctx.bufnr
end
@@ -589,7 +589,7 @@ local function make_type_hierarchy_handler()
local pos =
vim.pos.lsp(assert(ctx.bufnr), type_hierarchy_item.range.start, client.offset_encoding)
table.insert(items, {
filename = vim.uri_to_fname(type_hierarchy_item.uri),
filename = vim.uri.to_fname(type_hierarchy_item.uri),
text = format_item(type_hierarchy_item),
lnum = pos.row + 1,
col = pos.col + 1,
+2 -2
View File
@@ -8,13 +8,13 @@ local util = lsp.util
---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
---@return {name: string, filename: string, cmd: string, kind?: string}
local function mk_tag_item(name, range, uri, position_encoding)
local bufnr = vim.uri_to_bufnr(uri)
local bufnr = vim.uri.to_buf(uri)
-- This is get_line_byte_from_position is 0-indexed, call cursor expects a 1-indexed position
local pos = vim.pos.lsp(bufnr, range.start, position_encoding)
local byte = pos.col + 1
return {
name = name,
filename = vim.uri_to_fname(uri),
filename = vim.uri.to_fname(uri),
cmd = string.format([[/\%%%dl\%%%dc/]], range.start.line + 1, byte),
}
end
+13 -13
View File
@@ -402,7 +402,7 @@ function M.apply_text_document_edit(
validate('position_encoding', position_encoding, 'string')
local text_document = text_document_edit.textDocument
local bufnr = vim.uri_to_bufnr(text_document.uri)
local bufnr = vim.uri.to_buf(text_document.uri)
-- `VersionedTextDocumentIdentifier`s version may be null
-- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier
@@ -557,7 +557,7 @@ end
local function create_file(change)
local opts = change.options or {}
-- from spec: Overwrite wins over `ignoreIfExists`
local fname = vim.uri_to_fname(change.uri)
local fname = vim.uri.to_fname(change.uri)
if not opts.ignoreIfExists or opts.overwrite then
vim.fn.mkdir(vim.fs.dirname(fname), 'p')
local file = io.open(fname, 'w')
@@ -571,7 +571,7 @@ end
--- @param change lsp.DeleteFile
local function delete_file(change)
local opts = change.options or {}
local fname = vim.uri_to_fname(change.uri)
local fname = vim.uri.to_fname(change.uri)
local bufnr = vim.fn.bufadd(fname)
vim.fs.rm(fname, {
force = opts.ignoreIfNotExists,
@@ -593,7 +593,7 @@ function M.apply_workspace_edit(workspace_edit, position_encoding)
for idx, change in ipairs(workspace_edit.documentChanges) do
if change.kind == 'rename' then
local options = change.options --[[@as vim.lsp.util.rename.Opts]]
M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), options)
M.rename(vim.uri.to_fname(change.oldUri), vim.uri.to_fname(change.newUri), options)
elseif change.kind == 'create' then
create_file(change)
elseif change.kind == 'delete' then
@@ -601,7 +601,7 @@ function M.apply_workspace_edit(workspace_edit, position_encoding)
elseif change.kind then --- @diagnostic disable-line:undefined-field
error(string.format('Unsupported change: %q', vim.inspect(change)))
else
local bufnr = vim.uri_to_bufnr(change.textDocument.uri)
local bufnr = vim.uri.to_buf(change.textDocument.uri)
M.apply_text_document_edit(change, idx, position_encoding, workspace_edit.changeAnnotations)
-- avoid triggering OptionSet
if not vim.bo[bufnr].buflisted then
@@ -618,7 +618,7 @@ function M.apply_workspace_edit(workspace_edit, position_encoding)
end
for uri, changes in pairs(all_changes) do
local bufnr = vim.uri_to_bufnr(uri)
local bufnr = vim.uri.to_buf(uri)
M.apply_text_edits(changes, bufnr, position_encoding, workspace_edit.changeAnnotations)
-- avoid triggering OptionSet
if not vim.bo[bufnr].buflisted then
@@ -925,7 +925,7 @@ end
--- @param opts? vim.lsp.util.show_document.Opts
--- @return boolean
function M._show_document(uri, range, position_encoding, opts)
local bufnr = vim.uri_to_bufnr(uri)
local bufnr = vim.uri.to_buf(uri)
-- Return early if the buffer fails to load, to avoid partial setup.
local loaded, err = pcall(vim.fn.bufload, bufnr)
@@ -995,7 +995,7 @@ function M.preview_location(location, opts)
if uri == nil then
return
end
local bufnr = vim.uri_to_bufnr(uri)
local bufnr = vim.uri.to_buf(uri)
if not api.nvim_buf_is_loaded(bufnr) then
vim.fn.bufload(bufnr)
end
@@ -1802,7 +1802,7 @@ function M.locations_to_items(locations, position_encoding)
for uri, rows in vim.spairs(grouped) do
table.sort(rows, position_sort)
local filename = vim.uri_to_fname(uri)
local filename = vim.uri.to_fname(uri)
local line_numbers = {}
for _, temp in ipairs(rows) do
@@ -1813,7 +1813,7 @@ function M.locations_to_items(locations, position_encoding)
end
-- get all the lines for this uri
local lines = get_lines(vim.uri_to_bufnr(uri), line_numbers)
local lines = get_lines(vim.uri.to_buf(uri), line_numbers)
for _, temp in ipairs(rows) do
local pos = temp.start
@@ -1857,7 +1857,7 @@ function M.symbols_to_items(symbols, bufnr, position_encoding)
if symbol.location then
--- @cast symbol lsp.SymbolInformation
filename = vim.uri_to_fname(symbol.location.uri)
filename = vim.uri.to_fname(symbol.location.uri)
range = vim.range.lsp(bufnr, symbol.location.range, position_encoding)
elseif symbol.selectionRange then
--- @cast symbol lsp.DocumentSymbol
@@ -1962,7 +1962,7 @@ end
---@return lsp.TextDocumentIdentifier
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
function M.make_text_document_params(bufnr)
return { uri = vim.uri_from_bufnr(bufnr or 0) }
return { uri = vim.uri.from_buf(bufnr or 0) }
end
--- Create the workspace params
@@ -1997,7 +1997,7 @@ function M.make_formatting_params(options)
insertSpaces = vim.bo.expandtab,
}) --[[@as lsp.FormattingOptions]]
return {
textDocument = { uri = vim.uri_from_bufnr(0) },
textDocument = { uri = vim.uri.from_buf(0) },
options = options,
}
end
+1 -1
View File
@@ -111,7 +111,7 @@ local function match_link(line, pattern, link_type, lnum, src)
---@diagnostic disable-next-line: param-type-mismatch
target = require('vim._core.util').get_forge_url(src, match, link_type)
elseif link_type == 'path' then
target = vim.uri_from_fname(match)
target = vim.uri.from_fname(match)
end
if target == nil then
+1 -1
View File
@@ -83,7 +83,7 @@ function M.get_lines(buf, rows)
-- Load the buffer if this is not a file URI.
-- Custom language server protocol extensions can result in servers sending
-- URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
if not vim.startswith(vim.uri_from_bufnr(buf), 'file://') then
if not vim.startswith(vim.uri.from_buf(buf), 'file://') then
vim.fn.bufload(buf)
return buf_lines()
end
+5 -5
View File
@@ -182,8 +182,7 @@ end
---@see |vim.system()|
function M.open(path, opt)
vim.validate('path', path, 'string')
local is_uri = path:match('%w+:')
if not is_uri then
if not vim.uri.parse(path) then
path = vim.fs.normalize(path)
end
@@ -202,8 +201,9 @@ function M.open(path, opt)
if open_cmd[1] == 'xdg-open' then
job_opt.stdout = false
job_opt.stderr = false
elseif open_cmd[1] == 'cmd.exe' and is_uri then
path = path:gsub('([&|<>^%%!])', '^%1') -- Escape cmd.exe special chars. #41337
elseif open_cmd[1] == 'cmd.exe' and not path:find('[%s"]') then
-- Escape cmd.exe special chars, unless libuv quotes the arg (it has whitespace or '"').
path = path:gsub('([&|<>^%%!])', '^%1')
end
cmd = vim.list_extend(open_cmd, { path })
end
@@ -260,7 +260,7 @@ local get_lsp_urls = function(bufnr)
if document_link.target and range:has(position) then
local target = document_link.target ---@type string
if vim.startswith(target, 'file://') then
target = vim.uri_to_fname(target)
target = vim.uri.to_fname(target)
end
table.insert(urls, target)
end
+149 -61
View File
@@ -1,23 +1,19 @@
-- TODO: This is implemented only for files currently.
-- https://tools.ietf.org/html/rfc3986
-- https://tools.ietf.org/html/rfc2732
-- https://tools.ietf.org/html/rfc2396
-- https://tools.ietf.org/html/rfc8089
local M = {}
local sbyte = string.byte
local schar = string.char
local tohex = require('bit').tohex
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]:.*'
local iswin = package.config:sub(1, 1) == '\\'
local PATTERNS = {
-- RFC 2396
-- https://tools.ietf.org/html/rfc2396#section-2.2
-- RFC 2396 https://tools.ietf.org/html/rfc2396#section-2.3
rfc2396 = "^A-Za-z0-9%-_.!~*'()",
-- RFC 2732
-- https://tools.ietf.org/html/rfc2732
-- RFC 2732 https://tools.ietf.org/html/rfc2732
rfc2732 = "^A-Za-z0-9%-_.!~*'()%[%]",
-- RFC 3986
-- https://tools.ietf.org/html/rfc3986#section-2.2
-- RFC 3986 https://tools.ietf.org/html/rfc3986#section-3.3
rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/",
}
@@ -34,97 +30,189 @@ local function percent_encode_char(char)
return '%' .. tohex(sbyte(char), 2)
end
local uri_mt = {
--- @param uri vim.Uri
--- @return string
__tostring = function(uri)
return ('%s:%s%s%s%s'):format(
uri.scheme,
uri.authority and ('//%s'):format(uri.authority) or '',
uri.path,
uri.query and ('?%s'):format(uri.query) or '',
uri.fragment and ('#%s'):format(uri.fragment) or ''
)
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
--- 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 boolean
local function is_windows_file_uri(uri)
return uri:match('^file:/+[a-zA-Z]:') ~= nil
---@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)
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)
local query_i = rest:find('?', 1, true)
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({
scheme = scheme,
authority = authority,
path = path or rest,
query = query,
fragment = fragment,
}, uri_mt)
end
---URI-encodes a string using percent escapes.
---URI-encodes a string.
---@since 15
---@param str string string to encode
---@param rfc "rfc2396" | "rfc2732" | "rfc3986" | nil
---@return string encoded string
function M.uri_encode(str, rfc)
function M.encode(str, rfc)
local pattern = PATTERNS[rfc] or PATTERNS.rfc3986
return (str:gsub('([' .. pattern .. '])', percent_encode_char)) -- clamped to 1 retval with ()
end
---URI-decodes a string containing percent escapes.
---Decodes a URI-encoded string.
---@since 15
---@param str string string to decode
---@return string decoded string
function M.uri_decode(str)
function M.decode(str)
return (str:gsub('%%([a-fA-F0-9][a-fA-F0-9])', hex_to_char)) -- clamped to 1 retval with ()
end
---Gets a URI from a file path.
---Gets a URI from a file path. 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")
---```
---
---@since 15
---@param path string Path to file
---@return string URI
function M.uri_from_fname(path)
local volume_path, fname = path:match('^([a-zA-Z]:)(.*)') ---@type string?, string?
local is_windows = volume_path ~= nil
if is_windows then
assert(fname)
path = volume_path .. M.uri_encode((fname:gsub('\\', '/')))
else
path = M.uri_encode(path)
function M.from_fname(path)
if iswin or path:find('^%a:') then
path = path:gsub('\\', '/') ---@type string
end
local uri_parts = { 'file://' }
if is_windows then
table.insert(uri_parts, '/')
if path ~= '' and not path:find('^/') and not path:find('^%a:') then
path = vim.fs.normalize(vim.fs.abspath(path, { plain = true }), { plain = true }) -- Force absolute, RFC 8089 disallows relative.
end
table.insert(uri_parts, path)
return table.concat(uri_parts)
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)))
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))
end
---Gets a URI from a bufnr.
---Gets a URI from a buffer.
---@since 15
---@param buf integer
---@return string URI
function M.uri_from_bufnr(buf)
function M.from_buf(buf)
local fname = vim.api.nvim_buf_get_name(buf)
local volume_path = fname:match('^([a-zA-Z]:).*')
local is_windows = volume_path ~= nil
local scheme ---@type string?
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
if M.parse(fname) then
return fname
else
return M.uri_from_fname(fname)
end
return M.from_fname(fname)
end
---Gets a filename from a URI.
---Gets a filename from a URI. Drops the query and fragment, so "?" and "#" in a filename must be
---percent-encoded.
---
---Example:
---
---```lua
---vim.uri.to_fname('file:///a%3Fb?c#d') --> '/a?b'
---vim.uri.to_fname('file:///C:/a/b') --> 'C:/a/b'
---vim.uri.to_fname('file://server/share/a') --> '//server/share/a'
---vim.uri.to_fname('jdt://a/b') --> 'jdt://a/b'
---```
---
---@since 15
---@param uri string
---@return string filename or unchanged URI for non-file URIs
function M.uri_to_fname(uri)
local scheme = uri:match(URI_SCHEME_PATTERN) or error('URI must contain a scheme: ' .. uri)
if scheme ~= 'file' then
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
end
local fragment_index = uri:find('#')
if fragment_index ~= nil then
uri = uri:sub(1, fragment_index - 1)
parts = parts or error(('URI must contain a scheme: %s'):format(uri))
if parts.scheme:lower() ~= 'file' then
return uri
end
uri = M.uri_decode(uri)
--TODO improve this.
if is_windows_file_uri(uri) then
uri = uri:gsub('^file:/+', ''):gsub('/', '\\') --- @type string
else
uri = uri:gsub('^file:/+', '/') ---@type string
local host = M.decode(parts.authority 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
return uri
-- 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.
return ('//%s%s'):format(host, path)
end
-- Drive letter: "/C:/a" => "C:/a".
return path:match('^/(%a:.*)$') or path
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
---@return integer bufnr
function M.uri_to_bufnr(uri)
return vim.fn.bufadd(M.uri_to_fname(uri))
---@return integer buf
function M.to_buf(uri)
return vim.fn.bufadd(M.to_fname(uri))
end
return M
+1 -1
View File
@@ -104,7 +104,7 @@ api.nvim_create_autocmd('BufReadCmd', {
if legacy_loaded() then
return true
end
if ev.match:match('^%a[%w+.-]*://') then
if vim._core.path_is_url(ev.match) then
return
end
require('nvim.zip').browse(ev.buf, ev.match)
+1
View File
@@ -81,6 +81,7 @@ pub fn nvim_gen_sources(
"loader",
"text",
"tty",
"uri",
};
for (names) |n| {
gen_step.addFileArg(b.path(b.fmt("runtime/lua/vim/{s}.lua", .{n})));
+1 -1
View File
@@ -243,7 +243,7 @@ local config = {
'runtime/lua/vim/version.lua',
},
fn_xform = function(fun)
if contains(fun.module, { 'vim.uri', 'vim._core.shared', 'vim._core.editor' }) then
if contains(fun.module, { 'vim._core.shared', 'vim._core.editor' }) then
fun.module = 'vim'
end
-4
View File
@@ -59,10 +59,6 @@ local legacy_names = {
nvim_list_uis = true,
nvim_list_wins = true,
},
['runtime/lua/vim/uri.lua'] = {
uri_from_bufnr = true,
uri_to_bufnr = true,
},
['runtime/lua/vim/_core/shared.lua'] = {
_ensure_list = true,
_list_insert = true,
+3
View File
@@ -353,6 +353,7 @@ set(LUA_KEYMAP_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/keymap.lua)
set(LUA_LOADER_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/loader.lua)
set(LUA_TEXT_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/text.lua)
set(LUA_TTY_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/tty.lua)
set(LUA_URI_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/uri.lua)
# Find all Lua modules in the _core/ directory.
file(GLOB LUA_CORE_MODULE_SOURCES ${NVIM_RUNTIME_DIR}/lua/vim/_core/*.lua)
@@ -655,6 +656,7 @@ add_custom_command(
${LUA_LOADER_MODULE_SOURCE} "vim.loader"
${LUA_TEXT_MODULE_SOURCE} "vim.text"
${LUA_TTY_MODULE_SOURCE} "vim.tty"
${LUA_URI_MODULE_SOURCE} "vim.uri"
${LUA_CORE_COMMAND_ARGS}
DEPENDS
${CHAR_BLOB_GENERATOR}
@@ -667,6 +669,7 @@ add_custom_command(
${LUA_LOADER_MODULE_SOURCE}
${LUA_TEXT_MODULE_SOURCE}
${LUA_TTY_MODULE_SOURCE}
${LUA_URI_MODULE_SOURCE}
${LUA_CORE_MODULE_SOURCES}
VERBATIM
)
+14
View File
@@ -43,6 +43,7 @@
#include "nvim/memline.h"
#include "nvim/memory.h"
#include "nvim/message.h"
#include "nvim/path.h"
#include "nvim/pos_defs.h"
#include "nvim/regexp.h"
#include "nvim/regexp_defs.h"
@@ -466,6 +467,13 @@ int nlua_getvar(lua_State *lstate)
return 1;
}
/// Checks if a path is a URL. See path_with_url().
static int nlua_path_is_url(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
{
lua_pushboolean(lstate, path_with_url(luaL_checkstring(lstate, 1)));
return 1;
}
/// Compare two strings, ignoring case
///
/// Expects two values on the stack: compared strings. Returns one of the
@@ -844,6 +852,12 @@ void nlua_state_add_stdlib(lua_State *const lstate, bool is_thread)
lua_cjson_new(lstate);
lua_setfield(lstate, -2, "json");
// vim._core.path_is_url
lua_getfield(lstate, -1, "_core");
lua_pushcfunction(lstate, &nlua_path_is_url);
lua_setfield(lstate, -2, "path_is_url");
lua_pop(lstate, 1);
#ifdef NVIM_VENDOR_BIT
// if building with puc lua, use internal fallback for require'bit'
int top = lua_gettop(lstate);
+12 -24
View File
@@ -36,11 +36,6 @@
#include "nvim/strings.h"
#include "nvim/vim_defs.h"
enum {
URL_SLASH = 1, // path_is_url() has found ":/"
URL_BACKSLASH = 2, // path_is_url() has found ":\\"
};
#ifdef gen_expand_wildcards
# undef gen_expand_wildcards
#endif
@@ -1690,38 +1685,31 @@ bool path_has_drive_letter(const char *p, size_t path_len)
&& (path_len == 2 || ((p[2] == '/') | (p[2] == '\\') | (p[2] == '?') | (p[2] == '#')));
}
// Check if the ":/" of a URL is at the pointer, return URL_SLASH.
// Also check for ":\\", which MS Internet Explorer accepts, return
// URL_BACKSLASH.
int path_is_url(const char *p)
/// Checks if the ":/" of a URL is at the pointer.
bool path_is_url(const char *p)
FUNC_ATTR_NONNULL_ALL
{
// In the spec ':' is enough to recognize a scheme
// https://url.spec.whatwg.org/#scheme-state
if (strncmp(p, ":/", 2) == 0) {
return URL_SLASH;
} else if (strncmp(p, ":\\\\", 3) == 0) {
return URL_BACKSLASH;
}
return 0;
return strncmp(p, ":/", 2) == 0;
}
/// Check if "fname" starts with "name:/" or "name:\".
/// Checks if `fname` is a URL (not a filepath).
///
/// - "deno:/a" and "fugitive:///a" are URLs
/// - "C:/a" (drive letter) and "mailto:a" are NOT.
///
/// @param fname is the filename to test
/// @return URL_SLASH for "name:/", URL_BACKSLASH for "name:\", zero otherwise.
int path_with_url(const char *fname)
bool path_with_url(const char *fname)
FUNC_ATTR_NONNULL_ALL
{
const char *p;
// first character must be alpha
if (!ASCII_ISALPHA(*fname)) {
return 0;
return false;
}
if (path_has_drive_letter(fname, strlen(fname))) {
return 0;
return false;
}
// check body: (alpha, digit, '+', '-', '.') following RFC3986
@@ -1729,10 +1717,10 @@ int path_with_url(const char *fname)
// check last char is not '+', '-', or '.'
if ((p[-1] == '+') || (p[-1] == '-') || (p[-1] == '.')) {
return 0;
return false;
}
// ":/" or ":\\" must follow
// ":/" must follow
return path_is_url(p);
}
+1
View File
@@ -233,6 +233,7 @@ describe('vim._core', function()
'vim.loader',
'vim.text',
'vim.tty',
'vim.uri',
}
if n.exec_lua [[return not not _G.jit]] then
expected = vim.list_extend({
+1 -1
View File
@@ -3688,7 +3688,7 @@ describe('vim.diagnostic', function()
-- Populate the related info buffer.
exec_lua(function()
local fake_bufnr = vim.uri_to_bufnr(related_info_uri)
local fake_bufnr = vim.uri.to_buf(related_info_uri)
vim.fn.bufload(fake_bufnr)
vim.api.nvim_buf_set_lines(fake_bufnr, 0, 1, false, {
'O, the Pelican,',
+20 -3
View File
@@ -84,16 +84,33 @@ describe('vim.filetype', function()
)
end)
it('matches URI buffer patterns ending in slash', function()
it('matches URI buffer names as-is #42598', function()
eq(
'example',
{ 'example', 'myft', 'fugitive:///repo/a.sql', 'deno:/https/deno.land/a.sql' },
exec_lua(function()
vim.filetype.add({
filename = {
['myscheme://project/foo'] = 'myft',
},
pattern = {
['example://.*'] = 'example',
-- Detects the path passed to filetype functions as the filetype.
['.*%.sql'] = function(path)
return path
end,
},
})
return vim.filetype.match({ filename = 'example:///tmp/example/' })
local names = {
'example:///tmp/example/',
'myscheme://project/foo',
'fugitive:///repo/a.sql',
'deno:/https/deno.land/a.sql',
}
local fts = {} ---@type string[]
for i, name in ipairs(names) do
fts[i] = vim.filetype.match({ filename = name })
end
return fts
end)
)
end)
+21 -2
View File
@@ -619,6 +619,18 @@ describe('vim.fs', function()
it('works with a filename argument', function()
eq(test_source_path, exec_lua([[return vim.fs.root(..., 'CMakePresets.json')]], nvim_prog))
eq_paths(
test_source_path,
exec_lua([[return vim.fs.root(vim.uri.from_fname(...), 'CMakePresets.json')]], nvim_prog)
)
-- Scheme is case-insensitive.
eq_paths(
test_source_path,
exec_lua(
[[return vim.fs.root(vim.uri.from_fname(...):gsub('^file:', 'FILE:'), 'CMakePresets.json')]],
nvim_prog
)
)
end)
it('works with a relative path', function()
@@ -642,11 +654,11 @@ describe('vim.fs', function()
eq_paths(test_source_path, exec_lua([[return vim.fs.root(0, 'CMakePresets.json')]]))
end)
it('returns CWD (absolute path) if no match is found', function()
it('returns CWD (absolute path) for a URL', function()
assert(n.fn.isabsolutepath(test_source_path) == 1)
eq_paths(
test_source_path,
exec_lua([[return vim.fs.root('file://bogus', 'CMakePresets.json')]])
exec_lua([[return vim.fs.root('foo://bogus', 'CMakePresets.json')]])
)
end)
end)
@@ -930,6 +942,13 @@ describe('vim.fs', function()
eq('/foo/../.', vim.fs.abspath('/foo/../.'))
eq('/foo/bar', vim.fs.abspath('/foo/bar'))
end
eq('fugitive:///repo/.git//0/a', vim.fs.abspath('fugitive:///repo/.git//0/a'))
eq('deno:/https/deno.land/a', vim.fs.abspath('deno:/https/deno.land/a'))
-- Drive letter, not a URL scheme.
eq(
{ false, false },
{ vim._core.path_is_url([[C:\foo\bar]]), vim._core.path_is_url('C:/foo') }
)
end)
it('with `cwd`', function()
+59 -241
View File
@@ -8,10 +8,31 @@ local clear = n.clear
local feed = n.feed
local eq = t.eq
local exec_lua = n.exec_lua
local next_msg = n.next_msg
local NIL = vim.NIL
local pcall_err = t.pcall_err
--- Calls `vim.*` functions in a thread. Returns the results as an mpack-encoded table.
local vim_fns = [[
return vim.mpack.encode({
diff = vim.diff('Hello\n', 'Helli\n'),
is_thread = vim.is_thread(),
json = vim.json.decode(vim.json.encode({ 33, vim.NIL, 'text' })),
mpack = vim.mpack.decode(vim.mpack.encode({ 33, vim.NIL, 'text' })),
path_is_url = vim._core.path_is_url('deno:/a'),
uri = vim.uri.to_fname('file:///a') .. vim.uri_to_fname('file:///b'),
uv = vim.uv.version() >= 72961,
})
]]
local vim_fns_expected = {
diff = '@@ -1 +1 @@\n-Hello\n+Helli\n',
is_thread = true,
json = { 33, NIL, 'text' },
mpack = { 33, NIL, 'text' },
path_is_url = true,
uri = '/a/b',
uv = true,
}
describe('thread', function()
local screen
@@ -119,150 +140,31 @@ describe('thread', function()
end)
end)
describe('vim.*', function()
before_each(function()
clear()
exec_lua [[
Thread_Test = {}
Thread_Test.entry_func = function(async, entry_str, args)
local decoded_args = vim.mpack.decode(args)
assert(loadstring(entry_str))(async, decoded_args)
end
function Thread_Test:do_test()
local async
local on_async = self.on_async
async = vim.uv.new_async(function(ret)
on_async(ret)
async:close()
end)
local thread =
vim.uv.new_thread(self.entry_func, async, self.entry_str, self.args)
vim.uv.thread_join(thread)
end
Thread_Test.new = function(entry, on_async, ...)
self = {}
setmetatable(self, {__index = Thread_Test})
self.args = vim.mpack.encode({...})
self.entry_str = string.dump(entry)
self.on_async = on_async
return self
end
]]
end)
it('is_thread', function()
exec_lua [[
local entry = function(async)
async:send(vim.is_thread())
end
local on_async = function(ret)
vim.rpcnotify(1, 'result', ret)
end
local thread_test = Thread_Test.new(entry, on_async)
thread_test:do_test()
]]
eq({ 'notification', 'result', { true } }, next_msg())
end)
it('uv', function()
exec_lua [[
local entry = function(async)
async:send(vim.uv.version())
end
local on_async = function(ret)
vim.rpcnotify(1, ret)
end
local thread_test = Thread_Test.new(entry, on_async)
thread_test:do_test()
]]
local msg = next_msg()
eq('notification', msg[1])
assert(tonumber(msg[2]) >= 72961)
end)
it('mpack', function()
exec_lua [[
local entry = function(async)
async:send(vim.mpack.encode({33, vim.NIL, 'text'}))
end
local on_async = function(ret)
vim.rpcnotify(1, 'result', vim.mpack.decode(ret))
end
local thread_test = Thread_Test.new(entry, on_async)
thread_test:do_test()
]]
eq({ 'notification', 'result', { { 33, NIL, 'text' } } }, next_msg())
end)
it('json', function()
exec_lua [[
local entry = function(async)
async:send(vim.json.encode({33, vim.NIL, 'text'}))
end
local on_async = function(ret)
vim.rpcnotify(1, 'result', vim.json.decode(ret))
end
local thread_test = Thread_Test.new(entry, on_async)
thread_test:do_test()
]]
eq({ 'notification', 'result', { { 33, NIL, 'text' } } }, next_msg())
end)
it('diff', function()
exec_lua [[
local entry = function(async)
async:send(vim.diff('Hello\n', 'Helli\n'))
end
local on_async = function(ret)
vim.rpcnotify(1, 'result', ret)
end
local thread_test = Thread_Test.new(entry, on_async)
thread_test:do_test()
]]
eq({
'notification',
'result',
{
table.concat({
'@@ -1 +1 @@',
'-Hello',
'+Helli',
'',
}, '\n'),
},
}, next_msg())
end)
it('vim.* functions', function()
eq(
vim_fns_expected,
exec_lua(function()
local result --- @type table?
local async = assert(vim.uv.new_async(function(ret)
result = vim.mpack.decode(ret)
end))
local thread = assert(vim.uv.new_thread(function(async_, code)
async_:send(assert(loadstring(code))())
end, async, vim_fns))
vim.uv.thread_join(thread)
vim.wait(2000, function()
return result ~= nil
end)
async:close()
return result
end)
)
end)
end)
describe('threadpool', function()
before_each(clear)
it('is_thread', function()
eq(false, exec_lua [[return vim.is_thread()]])
exec_lua [[
local work_fn = function()
return vim.is_thread()
end
local after_work_fn = function(ret)
vim.rpcnotify(1, 'result', ret)
end
local work = vim.uv.new_work(work_fn, after_work_fn)
work:queue()
]]
eq({ 'notification', 'result', { true } }, next_msg())
end)
it('with invalid argument', function()
local status = pcall_err(
exec_lua,
@@ -293,107 +195,23 @@ describe('threadpool', function()
]])
end)
describe('vim.*', function()
before_each(function()
clear()
exec_lua [[
Threadpool_Test = {}
Threadpool_Test.work_fn = function(work_fn_str, args)
local decoded_args = vim.mpack.decode(args)
return assert(loadstring(work_fn_str))(decoded_args)
end
function Threadpool_Test:do_test()
local work =
vim.uv.new_work(self.work_fn, self.after_work)
work:queue(self.work_fn_str, self.args)
end
Threadpool_Test.new = function(work_fn, after_work, ...)
self = {}
setmetatable(self, {__index = Threadpool_Test})
self.args = vim.mpack.encode({...})
self.work_fn_str = string.dump(work_fn)
self.after_work = after_work
return self
end
]]
end)
it('uv', function()
exec_lua [[
local work_fn = function()
return vim.uv.version()
end
local after_work_fn = function(ret)
vim.rpcnotify(1, ret)
end
local threadpool_test = Threadpool_Test.new(work_fn, after_work_fn)
threadpool_test:do_test()
]]
local msg = next_msg()
eq('notification', msg[1])
assert(tonumber(msg[2]) >= 72961)
end)
it('mpack', function()
exec_lua [[
local work_fn = function()
local var = vim.mpack.encode({33, vim.NIL, 'text'})
return var
end
local after_work_fn = function(ret)
vim.rpcnotify(1, 'result', vim.mpack.decode(ret))
end
local threadpool_test = Threadpool_Test.new(work_fn, after_work_fn)
threadpool_test:do_test()
]]
eq({ 'notification', 'result', { { 33, NIL, 'text' } } }, next_msg())
end)
it('json', function()
exec_lua [[
local work_fn = function()
local var = vim.json.encode({33, vim.NIL, 'text'})
return var
end
local after_work_fn = function(ret)
vim.rpcnotify(1, 'result', vim.json.decode(ret))
end
local threadpool_test = Threadpool_Test.new(work_fn, after_work_fn)
threadpool_test:do_test()
]]
eq({ 'notification', 'result', { { 33, NIL, 'text' } } }, next_msg())
end)
it('work', function()
exec_lua [[
local work_fn = function()
return vim.diff('Hello\n', 'Helli\n')
end
local after_work_fn = function(ret)
vim.rpcnotify(1, 'result', ret)
end
local threadpool_test = Threadpool_Test.new(work_fn, after_work_fn)
threadpool_test:do_test()
]]
eq({
'notification',
'result',
{
table.concat({
'@@ -1 +1 @@',
'-Hello',
'+Helli',
'',
}, '\n'),
},
}, next_msg())
end)
it('vim.* functions', function()
eq(false, exec_lua('return vim.is_thread()'))
eq(
vim_fns_expected,
exec_lua(function()
local result --- @type table?
local work = vim.uv.new_work(function(code)
return assert(loadstring(code))()
end, function(ret)
result = vim.mpack.decode(ret)
end)
work:queue(vim_fns)
vim.wait(2000, function()
return result ~= nil
end)
return result
end)
)
end)
end)
+22 -5
View File
@@ -129,9 +129,14 @@ describe('vim.ui', function()
)
end)
it('escapes cmd.exe metacharacters in URIs #41337', function()
it('escapes cmd.exe metacharacters #41337', function()
eq(
{ 'cmd.exe', '/c', 'start', '', 'https://example.com/?q=^&^|^<^>^^^%^!' },
{
{ 'cmd.exe', '/c', 'start', '', 'https://example.com/?q=^&^|^<^>^^^%^!' },
{ 'cmd.exe', '/c', 'start', '', 'C:/a^&b.pdf' },
-- Not escaped: libuv quotes it.
{ 'cmd.exe', '/c', 'start', '', 'C:/a b&c.pdf' },
},
exec_lua(function()
vim.fn.has = function(feat)
return feat == 'win32' and 1 or 0
@@ -145,8 +150,12 @@ describe('vim.ui', function()
end,
}
end
vim.ui.open('https://example.com/?q=&|<>^%!')
return captured
local cmds = {} --- @type string[][]
for _, path in ipairs({ 'https://example.com/?q=&|<>^%!', 'C:/a&b.pdf', 'C:/a b&c.pdf' }) do
vim.ui.open(path)
cmds[#cmds + 1] = captured
end
return cmds
end)
)
end)
@@ -172,6 +181,14 @@ describe('vim.ui', function()
return cmd:wait()
end, { n.testprg('printargs-test'), 'arg1' })
)
-- Expands a path containing ":".
eq(
('arg1=%s/a:b;'):format(vim.fs.normalize('~')),
exec_lua(function(opener)
return vim.ui.open('~/a:b', { cmd = { opener } }):wait().stdout
end, n.testprg('printargs-test'))
)
end)
it('can reuse opt.cmd without accumulating paths', function()
@@ -205,7 +222,7 @@ describe('vim.ui', function()
local param = url:match('%?tag=(.*)')
local tagname =
n.api.nvim_buf_get_text(0, tag[2], tag[3], tag[4].end_row, tag[4].end_col, {})[1]
eq(vim.uri_encode(tagname), param)
eq(vim.uri.encode(tagname), param)
-- non-nvim tags are ignored
local buf = n.api.nvim_create_buf(false, false)
+236 -135
View File
@@ -9,203 +9,228 @@ local is_os = t.is_os
local skip = t.skip
local write_file = t.write_file
describe('URI methods', function()
describe('vim.uri', function()
before_each(function()
clear()
end)
describe('file path to uri', function()
describe('encode Unix file path', function()
it('file path includes only ascii characters', function()
exec_lua("filepath = '/Foo/Bar/Baz.txt'")
describe('from_fname()', function()
it('Unix filepath', function()
exec_lua("filepath = '/Foo/Bar/Baz.txt'")
eq('file:///Foo/Bar/Baz.txt', exec_lua('return vim.uri_from_fname(filepath)'))
end)
eq('file:///Foo/Bar/Baz.txt', exec_lua('return vim.uri.from_fname(filepath)'))
-- Resolves a relative path.
eq(
exec_lua(function()
local cwd = assert(vim.uv.cwd())
return {
vim.uri.from_fname(cwd .. '/a.txt'),
vim.uri.from_fname(vim.fs.dirname(cwd) .. '/b'),
}
end),
exec_lua(function()
return { vim.uri.from_fname('a.txt'), vim.uri.from_fname('../b') }
end)
)
it('file path including white space', function()
exec_lua("filepath = '/Foo /Bar/Baz.txt'")
exec_lua("filepath = '/Foo /Bar/Baz.txt'")
eq('file:///Foo%20/Bar/Baz.txt', exec_lua('return vim.uri.from_fname(filepath)'))
eq('file:///Foo%20/Bar/Baz.txt', exec_lua('return vim.uri_from_fname(filepath)'))
end)
it('file path including Unicode characters', function()
exec_lua("filepath = '/xy/åäö/ɧ/汉语/↥/🤦/🦄/å/بِيَّ.txt'")
-- The URI encoding should be case-insensitive
eq(
'file:///xy/%c3%a5%c3%a4%c3%b6/%c9%a7/%e6%b1%89%e8%af%ad/%e2%86%a5/%f0%9f%a4%a6/%f0%9f%a6%84/a%cc%8a/%d8%a8%d9%90%d9%8a%d9%8e%d9%91.txt',
exec_lua('return vim.uri_from_fname(filepath)')
)
end)
exec_lua("filepath = '/xy/åäö/ɧ/汉语/↥/🤦/🦄/å/بِيَّ.txt'")
-- The URI encoding should be case-insensitive
eq(
'file:///xy/%c3%a5%c3%a4%c3%b6/%c9%a7/%e6%b1%89%e8%af%ad/%e2%86%a5/%f0%9f%a4%a6/%f0%9f%a6%84/a%cc%8a/%d8%a8%d9%90%d9%8a%d9%8e%d9%91.txt',
exec_lua('return vim.uri.from_fname(filepath)')
)
end)
describe('encode Windows filepath', function()
it('file path includes only ascii characters', function()
exec_lua([[filepath = 'C:\\Foo\\Bar\\Baz.txt']])
it('Windows filepath', function()
exec_lua([[filepath = 'C:\\Foo\\Bar\\Baz.txt']])
eq('file:///C:/Foo/Bar/Baz.txt', exec_lua('return vim.uri.from_fname(filepath)'))
eq('file:///C:/Foo/Bar/Baz.txt', exec_lua('return vim.uri_from_fname(filepath)'))
end)
exec_lua([[filepath = 'C:\\Foo \\Bar\\Baz.txt']])
eq('file:///C:/Foo%20/Bar/Baz.txt', exec_lua('return vim.uri.from_fname(filepath)'))
it('file path including white space', function()
exec_lua([[filepath = 'C:\\Foo \\Bar\\Baz.txt']])
exec_lua([[filepath = 'C:\\xy\\åäö\\ɧ\\汉语\\↥\\🤦\\🦄\\å\\بِيَّ.txt']])
eq(
'file:///C:/xy/%c3%a5%c3%a4%c3%b6/%c9%a7/%e6%b1%89%e8%af%ad/%e2%86%a5/%f0%9f%a4%a6/%f0%9f%a6%84/a%cc%8a/%d8%a8%d9%90%d9%8a%d9%8e%d9%91.txt',
exec_lua('return vim.uri.from_fname(filepath)')
)
end)
eq('file:///C:/Foo%20/Bar/Baz.txt', exec_lua('return vim.uri_from_fname(filepath)'))
end)
it('file path including Unicode characters', function()
exec_lua([[filepath = 'C:\\xy\\åäö\\ɧ\\汉语\\↥\\🤦\\🦄\\å\\بِيَّ.txt']])
eq(
'file:///C:/xy/%c3%a5%c3%a4%c3%b6/%c9%a7/%e6%b1%89%e8%af%ad/%e2%86%a5/%f0%9f%a4%a6/%f0%9f%a6%84/a%cc%8a/%d8%a8%d9%90%d9%8a%d9%8e%d9%91.txt',
exec_lua('return vim.uri_from_fname(filepath)')
)
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' }
eq(
paths,
exec_lua(function()
return vim.tbl_map(function(path)
return vim.uri.to_fname(vim.uri.from_fname(path))
end, paths)
end)
)
end)
end)
describe('uri to filepath', function()
describe('decode Unix file path', function()
it('file path includes only ascii characters', function()
exec_lua("uri = 'file:///Foo/Bar/Baz.txt'")
describe('to_fname()', function()
it('Unix filepath', function()
exec_lua("uri = 'file:///Foo/Bar/Baz.txt'")
eq('/Foo/Bar/Baz.txt', exec_lua('return vim.uri_to_fname(uri)'))
end)
eq('/Foo/Bar/Baz.txt', exec_lua('return vim.uri.to_fname(uri)'))
it('local file path without hostname', function()
exec_lua("uri = 'file:/Foo/Bar/Baz.txt'")
-- Filepath without hostname:
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' },
exec_lua(function()
local uris = {
'file:Foo/Bar',
'file:////Foo/Bar',
'file://localhost/Foo/Bar',
'file://',
'FILE:///Foo',
}
return vim.tbl_map(vim.uri.to_fname, uris)
end)
)
eq('/Foo/Bar/Baz.txt', exec_lua('return vim.uri_to_fname(uri)'))
end)
-- Filepath with whitespace:
exec_lua("uri = 'file:///Foo%20/Bar/Baz.txt'")
eq('/Foo /Bar/Baz.txt', exec_lua('return vim.uri.to_fname(uri)'))
it('file path including white space', function()
exec_lua("uri = 'file:///Foo%20/Bar/Baz.txt'")
local test_case = [[
local uri = 'file:///xy/%C3%A5%C3%A4%C3%B6/%C9%A7/%E6%B1%89%E8%AF%AD/%E2%86%A5/%F0%9F%A4%A6/%F0%9F%A6%84/a%CC%8A/%D8%A8%D9%90%D9%8A%D9%8E%D9%91.txt'
return vim.uri.to_fname(uri)
]]
eq('/xy/åäö/ɧ/汉语/↥/🤦/🦄/å/بِيَّ.txt', exec_lua(test_case))
eq('/Foo /Bar/Baz.txt', exec_lua('return vim.uri_to_fname(uri)'))
end)
it('file path including Unicode characters', function()
local test_case = [[
local uri = 'file:///xy/%C3%A5%C3%A4%C3%B6/%C9%A7/%E6%B1%89%E8%AF%AD/%E2%86%A5/%F0%9F%A4%A6/%F0%9F%A6%84/a%CC%8A/%D8%A8%D9%90%D9%8A%D9%8E%D9%91.txt'
return vim.uri_to_fname(uri)
]]
eq('/xy/åäö/ɧ/汉语/↥/🤦/🦄/å/بِيَّ.txt', exec_lua(test_case))
end)
it('file path with uri fragment', function()
exec_lua("uri = 'file:///Foo/Bar/Baz.txt#fragment'")
eq('/Foo/Bar/Baz.txt', exec_lua('return vim.uri_to_fname(uri)'))
end)
-- Filepath with uri query, fragment.
exec_lua("uri = 'file:///Foo/Bar/Baz.txt?query#fragment'")
eq('/Foo/Bar/Baz.txt', exec_lua('return vim.uri.to_fname(uri)'))
end)
describe('decode Windows filepath', function()
it('file path includes only ascii characters', function()
describe('Windows filepath', function()
it('filepath includes only ascii characters', function()
local test_case = [[
local uri = 'file:///C:/Foo/Bar/Baz.txt'
return vim.uri_to_fname(uri)
return vim.uri.to_fname(uri)
]]
eq('C:\\Foo\\Bar\\Baz.txt', exec_lua(test_case))
eq('C:/Foo/Bar/Baz.txt', exec_lua(test_case))
end)
it('local file path without hostname', function()
it('local filepath without hostname', function()
local test_case = [[
local uri = 'file:/C:/Foo/Bar/Baz.txt'
return vim.uri_to_fname(uri)
return vim.uri.to_fname(uri)
]]
eq('C:\\Foo\\Bar\\Baz.txt', exec_lua(test_case))
eq('C:/Foo/Bar/Baz.txt', exec_lua(test_case))
-- Drive letter, not a host.
eq(
{ 'C:/Foo/Bar/Baz.txt', 'C:\\Foo\\Bar', 'C:\\Foo\\Bar' },
exec_lua(function()
local uris = { 'file://C:/Foo/Bar/Baz.txt', 'file://C:\\Foo\\Bar', 'C:\\Foo\\Bar' }
return vim.tbl_map(vim.uri.to_fname, uris)
end)
)
end)
it('file path includes only ascii characters with encoded colon character', function()
it('filepath includes only ascii characters with encoded colon character', function()
local test_case = [[
local uri = 'file:///C%3A/Foo/Bar/Baz.txt'
return vim.uri_to_fname(uri)
return vim.uri.to_fname(uri)
]]
eq('C:\\Foo\\Bar\\Baz.txt', exec_lua(test_case))
eq('C:/Foo/Bar/Baz.txt', exec_lua(test_case))
end)
it('file path including white space', function()
it('filepath including white space', function()
local test_case = [[
local uri = 'file:///C:/Foo%20/Bar/Baz.txt'
return vim.uri_to_fname(uri)
return vim.uri.to_fname(uri)
]]
eq('C:\\Foo \\Bar\\Baz.txt', exec_lua(test_case))
eq('C:/Foo /Bar/Baz.txt', exec_lua(test_case))
end)
it('file path including Unicode characters', function()
it('filepath including Unicode characters', function()
local test_case = [[
local uri = 'file:///C:/xy/%C3%A5%C3%A4%C3%B6/%C9%A7/%E6%B1%89%E8%AF%AD/%E2%86%A5/%F0%9F%A4%A6/%F0%9F%A6%84/a%CC%8A/%D8%A8%D9%90%D9%8A%D9%8E%D9%91.txt'
return vim.uri_to_fname(uri)
return vim.uri.to_fname(uri)
]]
eq('C:\\xy\\åäö\\ɧ\\汉语\\↥\\🤦\\🦄\\å\\بِيَّ.txt', exec_lua(test_case))
eq('C:/xy/åäö/ɧ/汉语/↥/🤦/🦄/å/بِيَّ.txt', exec_lua(test_case))
end)
it('filepath with host is a UNC path', function()
eq('//server/share/a b', exec_lua([[return vim.uri.to_fname('file://server/share/a%20b')]]))
if is_os('win') then
eq(
'file://server/share/a%20b',
exec_lua([[return vim.uri.from_fname('\\\\server\\share\\a b')]])
)
end
end)
end)
describe('decode non-file URI', function()
it('uri_to_fname returns non-file URI unchanged', function()
eq(
'jdt1.23+x-z://content/%5C/',
exec_lua [[
return vim.uri_to_fname('jdt1.23+x-z://content/%5C/')
]]
)
end)
it('non-file URI returns unchanged', function()
eq(
'jdt1.23+x-z://content/%5C/',
exec_lua [[
return vim.uri.to_fname('jdt1.23+x-z://content/%5C/')
]]
)
it('uri_to_fname returns non-file upper-case scheme URI unchanged', function()
eq(
'JDT://content/%5C/',
exec_lua [[
return vim.uri_to_fname('JDT://content/%5C/')
]]
)
end)
-- Upper-case scheme returns unchanged.
eq(
'JDT://content/%5C/',
exec_lua [[
return vim.uri.to_fname('JDT://content/%5C/')
]]
)
it('uri_to_fname returns non-file scheme URI without authority unchanged', function()
eq(
'zipfile:///path/to/archive.zip%3A%3Afilename.txt',
exec_lua [[
return vim.uri_to_fname('zipfile:///path/to/archive.zip%3A%3Afilename.txt')
]]
)
end)
-- Returns URI without authority unchanged.
eq(
'zipfile:///path/to/archive.zip%3A%3Afilename.txt',
exec_lua [[
return vim.uri.to_fname('zipfile:///path/to/archive.zip%3A%3Afilename.txt')
]]
)
end)
describe('decode URI without scheme', function()
it('fails because URI must have a scheme', function()
describe('URI without scheme', function()
it('fails (scheme is required)', function()
eq(
false,
exec_lua [[
return pcall(vim.uri_to_fname, 'not_an_uri.txt')
return pcall(vim.uri.to_fname, 'not_an_uri.txt')
]]
)
end)
it('uri_to_fname should not treat comma as a scheme character', function()
it('does not treat comma as a scheme character', function()
eq(
false,
exec_lua [[
return pcall(vim.uri_to_fname, 'foo,://bar')
return pcall(vim.uri.to_fname, 'foo,://bar')
]]
)
end)
it('uri_to_fname returns non-file schema URI with fragment unchanged', function()
it('returns non-file scheme URI with fragment unchanged', function()
eq(
'scheme://path#fragment',
exec_lua [[
return vim.uri_to_fname('scheme://path#fragment')
return vim.uri.to_fname('scheme://path#fragment')
]]
)
end)
end)
end)
describe('uri from bufnr', function()
describe('from_buf()', function()
it('Windows paths should not be treated as uris', function()
skip(not is_os('win'), 'N/A on non-Windows')
@@ -214,7 +239,7 @@ describe('URI methods', function()
local test_case = string.format(
[[
local file = '%s'
return vim.uri_from_bufnr(vim.fn.bufadd(file))
return vim.uri.from_buf(vim.fn.bufadd(file))
]],
file
)
@@ -224,41 +249,117 @@ describe('URI methods', function()
end)
end)
describe('uri to bufnr', function()
it('uri_to_bufnr & uri_from_bufnr returns original uri for non-file uris', function()
describe('to_buf()', function()
it('to_buf & from_buf returns original uri for non-file uris', function()
local uri =
'jdt://contents/java.base/java.util/List.class?=sql/%5C/home%5C/user%5C/.jabba%5C/jdk%5C/openjdk%5C@1.14.0%5C/lib%5C/jrt-fs.jar%60java.base=/javadoc_location=/https:%5C/%5C/docs.oracle.com%5C/en%5C/java%5C/javase%5C/14%5C/docs%5C/api%5C/=/%3Cjava.util(List.class'
local test_case = string.format(
[[
local uri = '%s'
return vim.uri_from_bufnr(vim.uri_to_bufnr(uri))
return vim.uri.from_buf(vim.uri.to_buf(uri))
]],
uri
)
eq(uri, exec_lua(test_case))
end)
it(
'uri_to_bufnr & uri_from_bufnr returns original uri for non-file uris without authority',
function()
local uri = 'zipfile:///path/to/archive.zip%3A%3Afilename.txt'
local test_case = string.format(
[[
it('to_buf & from_buf returns original uri for non-file uris without authority', function()
local uri = 'zipfile:///path/to/archive.zip%3A%3Afilename.txt'
local test_case = string.format(
[[
local uri = '%s'
return vim.uri_from_bufnr(vim.uri_to_bufnr(uri))
return vim.uri.from_buf(vim.uri.to_buf(uri))
]],
uri
)
eq(uri, exec_lua(test_case))
end
)
uri
)
eq(uri, exec_lua(test_case))
-- Deprecated aliases.
eq(uri, exec_lua('return vim.uri_from_bufnr(vim.uri_to_bufnr(...))', uri))
end)
end)
describe('encode to uri', function()
describe('encode()', function()
it('rfc2732 including brackets', function()
exec_lua("str = '[:]'")
exec_lua("rfc = 'rfc2732'")
eq('[%3a]', exec_lua('return vim.uri_encode(str, rfc)'))
eq('[%3a]', exec_lua('return vim.uri.encode(str, rfc)'))
end)
it('encode/decode roundtrip', function()
eq(
{ '/a:b@c$d%23%3f%25%20', '%2fa%3ab%40c%24d' },
exec_lua(function()
return { vim.uri.encode('/a:b@c$d#?% '), vim.uri.encode('/a:b@c$d', 'rfc2396') }
end)
)
-- Hex case, no double-decoding, malformed escapes.
eq(
'åå %41 %zz%',
exec_lua(function()
return vim.uri.decode('%c3%a5%C3%A5%20%2541%20%zz%')
end)
)
-- Roundtrips all bytes.
eq(
{},
exec_lua(function()
local bytes = {} ---@type string[]
for i = 0, 255 do
bytes[#bytes + 1] = string.char(i)
end
local s = table.concat(bytes)
local failed = {} ---@type string[]
for _, rfc in ipairs({ 'rfc2396', 'rfc2732', 'rfc3986' }) do
if vim.uri.decode(vim.uri.encode(s, rfc)) ~= s then
failed[#failed + 1] = rfc
end
end
return failed
end)
)
end)
end)
it('parse()', function()
local uris = {
'https://u@example.com:80/a%20b?q=1#f?#',
'file:///a',
'deno:/https/a',
'mailto:a@example.com',
'inmemory:',
'foo:api/files',
'f3ile:#d',
'file:////shares/p',
'stuff:?',
'C:/a',
'/a',
}
local parsed = exec_lua(function()
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)
return u or {}
end, uris)
end)
eq({
{
scheme = 'https',
authority = 'u@example.com:80',
path = '/a%20b',
query = 'q=1',
fragment = 'f?#',
},
{ scheme = 'file', authority = '', 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 = '' },
{},
{},
}, parsed)
end)
end)
+4 -4
View File
@@ -1533,7 +1533,7 @@ describe('vim.lsp.buf', function()
start = { line = 0, character = 0 },
['end'] = { line = 0, character = 0 },
},
uri = vim.uri_from_bufnr(bufnr),
uri = vim.uri.from_buf(bufnr),
}
callback(nil, location)
end,
@@ -1590,7 +1590,7 @@ describe('vim.lsp.buf', function()
start = { line = 0, character = character },
['end'] = { line = 0, character = character },
},
uri = vim.uri_from_bufnr(bufnr),
uri = vim.uri.from_buf(bufnr),
}
callback(nil, location)
end,
@@ -1704,7 +1704,7 @@ describe('vim.lsp.buf', function()
it('favors document diagnostics over workspace diagnostics', function()
local client_id = setup_server({ make_report('full', 'Workspace error', 1) })
local diagnostic_bufnr = exec_lua(function()
return vim.uri_to_bufnr(fake_uri)
return vim.uri.to_buf(fake_uri)
end)
exec_lua(function()
@@ -1740,7 +1740,7 @@ describe('vim.lsp.buf', function()
eq(
'Document error',
exec_lua(function()
return vim.diagnostic.get(vim.uri_to_bufnr(fake_uri))[1].message
return vim.diagnostic.get(vim.uri.to_buf(fake_uri))[1].message
end)
)
end)
@@ -96,7 +96,7 @@ describe('vim.lsp.diagnostic', function()
fake_uri = 'file:///fake/uri'
exec_lua(function()
diagnostic_bufnr = vim.uri_to_bufnr(fake_uri)
diagnostic_bufnr = vim.uri.to_buf(fake_uri)
local lines = { '1st line', '2nd line of text', 'wow', 'cool', 'more', 'lines' }
vim.fn.bufload(diagnostic_bufnr)
vim.api.nvim_buf_set_lines(diagnostic_bufnr, 0, 1, false, lines)
@@ -169,7 +169,7 @@ describe('vim.lsp.diagnostic', function()
uri = 'file:///fake/uri2',
diagnostics = {},
}, { client_id = client_id })
return vim.fn.bufnr(vim.uri_to_fname('file:///fake/uri2'))
return vim.fn.bufnr(vim.uri.to_fname('file:///fake/uri2'))
end)
)
@@ -183,7 +183,7 @@ describe('vim.lsp.diagnostic', function()
_G.make_error('Diagnostic', 0, 0, 0, 0),
},
}, { client_id = client_id })
return vim.fn.bufnr(vim.uri_to_fname('file:///fake/uri2'))
return vim.fn.bufnr(vim.uri.to_fname('file:///fake/uri2'))
end)
)
eq(
@@ -201,7 +201,7 @@ describe('vim.lsp.diagnostic', function()
uri = 'file:///fake/uri2',
diagnostics = {},
}, { client_id = client_id })
return vim.fn.bufnr(vim.uri_to_fname('file:///fake/uri2'))
return vim.fn.bufnr(vim.uri.to_fname('file:///fake/uri2'))
end)
)
eq(
@@ -766,7 +766,7 @@ describe('vim.lsp.diagnostic', function()
local fake_uri_2 = 'file:///fake/uri2'
---@type vim.Diagnostic[], vim.Diagnostic[], string?
local diagnostics, related_diagnostics, relatedPreviousResultId = exec_lua(function()
local second_buf = vim.uri_to_bufnr(fake_uri_2)
local second_buf = vim.uri.to_buf(fake_uri_2)
vim.fn.bufload(second_buf)
-- Attach the client to both buffers.
@@ -973,7 +973,7 @@ describe('vim.lsp.diagnostic', function()
_G.doc_requests,
vim.diagnostic.get(),
vim.diagnostic.get(diagnostic_bufnr),
vim.diagnostic.get(vim.uri_to_bufnr(fake_uri_3))
vim.diagnostic.get(vim.uri.to_buf(fake_uri_3))
end
)
eq(1, workspace_requests)
+6 -6
View File
@@ -266,7 +266,7 @@ describe('vim.lsp.util', function()
}
for _, params in ipairs(test_params) do
local actual = exec_lua(function(params0)
local bufnr = vim.uri_to_bufnr('file:///fake/uri')
local bufnr = vim.uri.to_buf('file:///fake/uri')
local lines = { 'testing', '123' }
vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
return vim.lsp.util.locations_to_items(params0, 'utf-16')
@@ -300,12 +300,12 @@ describe('vim.lsp.util', function()
},
}
local actual = exec_lua(function()
local bufnr = vim.uri_to_bufnr('file:///fake/uri')
local bufnr = vim.uri.to_buf('file:///fake/uri')
local lines = { 'testing', '123' }
vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
local locations = {
{
targetUri = vim.uri_from_bufnr(bufnr),
targetUri = vim.uri.from_buf(bufnr),
targetRange = {
start = { line = 0, character = 2 },
['end'] = { line = 0, character = 3 },
@@ -688,14 +688,14 @@ describe('vim.lsp.util', function()
before_each(function()
target_bufnr = exec_lua(function()
local bufnr = vim.uri_to_bufnr('file:///fake/uri')
local bufnr = vim.uri.to_buf('file:///fake/uri')
local lines = { '1st line of text', 'å å ɧ 汉语 ↥ 🤦 🦄' }
vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
return bufnr
end)
target_bufnr2 = exec_lua(function()
local bufnr = vim.uri_to_bufnr('file:///fake/uri2')
local bufnr = vim.uri.to_buf('file:///fake/uri2')
local lines = { '1st line of text', 'å å ɧ 汉语 ↥ 🤦 🦄' }
vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
return bufnr
@@ -1099,7 +1099,7 @@ describe('vim.lsp.util', function()
describe('stylize_markdown', function()
local stylize_markdown = function(content, opts)
return exec_lua(function()
local bufnr = vim.uri_to_bufnr('file:///fake/uri')
local bufnr = vim.uri.to_buf('file:///fake/uri')
vim.fn.bufload(bufnr)
return vim.lsp.util.stylize_markdown(bufnr, content, opts)
end)
+17 -17
View File
@@ -2615,7 +2615,7 @@ describe('LSP', function()
before_each(function()
target_bufnr = exec_lua(function()
local bufnr = vim.uri_to_bufnr('file:///fake/uri')
local bufnr = vim.uri.to_buf('file:///fake/uri')
local lines = { '1st line of text', '2nd line of 语text' }
vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
return bufnr
@@ -2746,7 +2746,7 @@ describe('LSP', function()
before_each(function()
exec_lua(function()
target_bufnr = vim.uri_to_bufnr('file:///fake/uri')
target_bufnr = vim.uri.to_buf('file:///fake/uri')
local lines = {
'Original Line #1',
'Original Line #2',
@@ -2813,7 +2813,7 @@ describe('LSP', function()
it('supports file creation with CreateFile payload', function()
local tmpfile = tmpname(false)
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
local uri = exec_lua('return vim.uri.from_fname(...)', tmpfile)
local edit = {
documentChanges = {
{
@@ -2832,7 +2832,7 @@ describe('LSP', function()
'supports file creation in folder that needs to be created with CreateFile payload',
function()
local tmpfile = tmpname(false) .. '/dummy/x/'
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
local uri = exec_lua('return vim.uri.from_fname(...)', tmpfile)
local edit = {
documentChanges = {
{
@@ -2851,7 +2851,7 @@ describe('LSP', function()
it('createFile does not touch file if it exists and ignoreIfExists is set', function()
local tmpfile = tmpname()
write_file(tmpfile, 'Dummy content')
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
local uri = exec_lua('return vim.uri.from_fname(...)', tmpfile)
local edit = {
documentChanges = {
{
@@ -2871,7 +2871,7 @@ describe('LSP', function()
it('createFile overrides file if overwrite is set', function()
local tmpfile = tmpname()
write_file(tmpfile, 'Dummy content')
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
local uri = exec_lua('return vim.uri.from_fname(...)', tmpfile)
local edit = {
documentChanges = {
{
@@ -2895,7 +2895,7 @@ describe('LSP', function()
local uri = exec_lua(function()
local bufnr = vim.fn.bufadd(tmpfile)
vim.fn.bufload(bufnr)
return vim.uri_from_fname(tmpfile)
return vim.uri.from_fname(tmpfile)
end)
local edit = {
documentChanges = {
@@ -2912,7 +2912,7 @@ describe('LSP', function()
it('DeleteFile fails if file does not exist and ignoreIfNotExists is false', function()
local tmpfile = tmpname(false)
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
local uri = exec_lua('return vim.uri.from_fname(...)', tmpfile)
local edit = {
documentChanges = {
{
@@ -4287,7 +4287,7 @@ describe('LSP', function()
},
{
globPattern = {
baseUri = vim.uri_from_fname(root_dir),
baseUri = vim.uri.from_fname(root_dir),
pattern = '{foo}',
},
kind = 7,
@@ -4341,7 +4341,7 @@ describe('LSP', function()
return { logfile = logfile, messages = server.messages, notifications = notifications }
end)
local uri = vim.uri_from_fname(root_dir .. '/watch')
local uri = vim.uri.from_fname(root_dir .. '/watch')
local messages = result.messages
eq(6, #messages)
@@ -4439,7 +4439,7 @@ describe('LSP', function()
eq({
method = 'workspace/didChangeWatchedFiles',
params = {
changes = { { uri = vim.uri_from_fname(root_dir .. '/file.py'), type = created } },
changes = { { uri = vim.uri.from_fname(root_dir .. '/file.py'), type = created } },
},
}, result)
end
@@ -4508,7 +4508,7 @@ describe('LSP', function()
end)
local function watched_uri(fname)
return vim.uri_from_fname(root_dir .. '/' .. fname)
return vim.uri.from_fname(root_dir .. '/' .. fname)
end
eq(4, #result)
@@ -4548,7 +4548,7 @@ describe('LSP', function()
for i = 0, max_kind do
table.insert(watchers, {
globPattern = {
baseUri = vim.uri_from_fname('/dir[1]'),
baseUri = vim.uri.from_fname('/dir[1]'),
pattern = 'watch' .. tostring(i),
},
kind = i,
@@ -4580,7 +4580,7 @@ describe('LSP', function()
end)
local function watched_uri(fname)
return vim.uri_from_fname('/dir[1]/' .. fname)
return vim.uri.from_fname('/dir[1]/' .. fname)
end
eq(3, #result)
@@ -4685,15 +4685,15 @@ describe('LSP', function()
changes = {
{
type = created,
uri = vim.uri_from_fname('file1'),
uri = vim.uri.from_fname('file1'),
},
{
type = changed,
uri = vim.uri_from_fname('file1'),
uri = vim.uri.from_fname('file1'),
},
{
type = created,
uri = vim.uri_from_fname('file2'),
uri = vim.uri.from_fname('file2'),
},
},
}, result[3].params)
+2 -2
View File
@@ -1590,10 +1590,10 @@ describe('vim.pack', function()
local fetch_src = repos_src.fetch
local fetch_path = pack_get_plug_path('fetch')
local fetch_path_uri = vim.uri_from_fname(fetch_path)
local fetch_path_uri = vim.uri.from_fname(fetch_path)
local semver_src = repos_src.semver
local semver_path = pack_get_plug_path('semver')
local semver_path_uri = vim.uri_from_fname(semver_path)
local semver_path_uri = vim.uri.from_fname(semver_path)
-- With `file://` sources it can only return "Path:" and "Source:" links
local ref_file_links = {
+5
View File
@@ -248,6 +248,11 @@ describe('nvim.zip', function()
eq({ 'plain text' }, lines())
eq(false, exec_lua('return vim.b.nvim_dir ~= nil'))
-- Ignores a URL.
n.command('edit deno:/x/a.zip')
poke_eventloop()
eq({ '', '' }, { vim.trim(n.exec_capture('messages')), n.eval('v:errmsg') })
end)
it('lists a valid empty archive as empty', function()
+4 -3
View File
@@ -1,7 +1,7 @@
" Test for the gf and gF (goto file) commands
" This is a test if a URL is recognized by "gf", with the cursor before and
" after the "://". Also test ":\\".
" after the "://". Nvim: ":\\" is not a URL.
func Test_gf_url()
enew!
call append(0, [
@@ -22,10 +22,11 @@ func Test_gf_url()
set isf=@,48-57,/,.,-,_,+,,,$,~,\
call search("^third")
call search("name")
call assert_equal("URL:\\\\machine.name\\vimtest2c", expand("<cfile>"))
" Nvim: Backslashes "\" not supported in URLs, thus "URL:" is ignored.
call assert_equal("\\\\machine.name\\vimtest2c", expand("<cfile>"))
call search("^fourth")
call search("URL")
call assert_equal("URL:\\\\machine.name\\tmp\\vimtest2d", expand("<cfile>"))
call assert_equal("URL", expand("<cfile>"))
call search("^fifth")
call search("URL")
+28 -27
View File
@@ -744,47 +744,48 @@ describe('path.c', function()
end
-- Check normal scheme with just alphabetic
eq(1, path_with_url([[test://xyz/foo/b0]]))
eq(2, path_with_url([[test:\\xyz\foo\b0]]))
eq(true, path_with_url([[test://xyz/foo/b0]]))
eq(false, path_with_url([[test:\\xyz\foo\b0]]))
-- Check valid scheme with just alphanumeric
eq(1, path_with_url([[test123://xyz/foo/b0]]))
eq(2, path_with_url([[test123:\\xyz\foo\b0]]))
eq(true, path_with_url([[test123://xyz/foo/b0]]))
eq(false, path_with_url([[test123:\\xyz\foo\b0]]))
-- Check invalid scheme (contains invalid character)
eq(0, path_with_url([[test_abc://xyz/foo/b2]]))
eq(false, path_with_url([[test_abc://xyz/foo/b2]]))
-- Check valid scheme containing '+', '-', or '.'
eq(1, path_with_url([[test+abc://xyz/foo/b1]]))
eq(2, path_with_url([[test+abc:\\xyz\foo\b1]]))
eq(1, path_with_url([[test-abc://xyz/foo/b3]]))
eq(2, path_with_url([[test-abc:\\xyz\foo\b3]]))
eq(1, path_with_url([[test.abc://xyz/foo/b1]]))
eq(2, path_with_url([[test.abc:\\xyz\foo\b1]]))
eq(true, path_with_url([[test+abc://xyz/foo/b1]]))
eq(false, path_with_url([[test+abc:\\xyz\foo\b1]]))
eq(true, path_with_url([[test-abc://xyz/foo/b3]]))
eq(false, path_with_url([[test-abc:\\xyz\foo\b3]]))
eq(true, path_with_url([[test.abc://xyz/foo/b1]]))
eq(false, path_with_url([[test.abc:\\xyz\foo\b1]]))
-- Check valid scheme with full suite of allowed characters
eq(1, path_with_url([[test+abc-123.ghi://xyz/foo/b1]]))
eq(2, path_with_url([[test+abc-123.ghi:\\xyz\foo\b1]]))
eq(true, path_with_url([[test+abc-123.ghi://xyz/foo/b1]]))
eq(false, path_with_url([[test+abc-123.ghi:\\xyz\foo\b1]]))
-- Check invalid scheme starting or ending with '+', '-', or '.'
eq(0, path_with_url([[-test://xyz/foo/b4]]))
eq(0, path_with_url([[test-://xyz/foo/b5]]))
eq(0, path_with_url([[+test://xyz/foo/b4]]))
eq(0, path_with_url([[test+://xyz/foo/b5]]))
eq(0, path_with_url([[.test://xyz/foo/b4]]))
eq(0, path_with_url([[test.://xyz/foo/b5]]))
eq(false, path_with_url([[-test://xyz/foo/b4]]))
eq(false, path_with_url([[test-://xyz/foo/b5]]))
eq(false, path_with_url([[+test://xyz/foo/b4]]))
eq(false, path_with_url([[test+://xyz/foo/b5]]))
eq(false, path_with_url([[.test://xyz/foo/b4]]))
eq(false, path_with_url([[test.://xyz/foo/b5]]))
-- Check additional valid scheme containing '+', '-', or '.'
eq(1, path_with_url([[test-C:/xyz/foo/b5]]))
eq(1, path_with_url([[test-custom:/xyz/foo/b5]]))
eq(1, path_with_url([[test+C:/xyz/foo/b5]]))
eq(1, path_with_url([[test+custom:/xyz/foo/b5]]))
eq(1, path_with_url([[test.C:/xyz/foo/b5]]))
eq(1, path_with_url([[test.custom:/xyz/foo/b5]]))
eq(true, path_with_url([[test-C:/xyz/foo/b5]]))
eq(true, path_with_url([[test-custom:/xyz/foo/b5]]))
eq(true, path_with_url([[test+C:/xyz/foo/b5]]))
eq(true, path_with_url([[test+custom:/xyz/foo/b5]]))
eq(true, path_with_url([[test.C:/xyz/foo/b5]]))
eq(true, path_with_url([[test.custom:/xyz/foo/b5]]))
-- Check invalid scheme representing drive letter
eq(0, path_with_url([[c:/xyz/foo/b5]]))
eq(0, path_with_url([[C:/xyz/foo/b5]]))
eq(false, path_with_url([[c:/xyz/foo/b5]]))
eq(false, path_with_url([[C:/xyz/foo/b5]]))
eq(false, path_with_url([[C:\xyz\foo\b5]]))
end)
end)
end)