diff --git a/runtime/doc/deprecated.txt b/runtime/doc/deprecated.txt index bd3835f451..ecdab2df68 100644 --- a/runtime/doc/deprecated.txt +++ b/runtime/doc/deprecated.txt @@ -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 diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 77e748d005..9df50386de 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -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`) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index cf58321845..29fcb5abbf 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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, diff --git a/runtime/ftplugin/help.lua b/runtime/ftplugin/help.lua index 4ad3a0c66a..d5af341091 100644 --- a/runtime/ftplugin/help.lua +++ b/runtime/ftplugin/help.lua @@ -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 diff --git a/runtime/lua/nvim/dir/fs.lua b/runtime/lua/nvim/dir/fs.lua index 9ff95ce8ad..688954c941 100644 --- a/runtime/lua/nvim/dir/fs.lua +++ b/runtime/lua/nvim/dir/fs.lua @@ -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 diff --git a/runtime/lua/nvim/zip.lua b/runtime/lua/nvim/zip.lua index c319c6a269..32e1f4b590 100644 --- a/runtime/lua/nvim/zip.lua +++ b/runtime/lua/nvim/zip.lua @@ -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) diff --git a/runtime/lua/vim/_core/editor.lua b/runtime/lua/vim/_core/editor.lua index d6457cc3e1..5a02f3075c 100644 --- a/runtime/lua/vim/_core/editor.lua +++ b/runtime/lua/vim/_core/editor.lua @@ -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, } diff --git a/runtime/lua/vim/_init_packages.lua b/runtime/lua/vim/_init_packages.lua index 5108a9fe70..a0dfcd2729 100644 --- a/runtime/lua/vim/_init_packages.lua +++ b/runtime/lua/vim/_init_packages.lua @@ -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, }) diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua index 6ae800e958..a19e4349d4 100644 --- a/runtime/lua/vim/_meta.lua +++ b/runtime/lua/vim/_meta.lua @@ -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') diff --git a/runtime/lua/vim/_meta/builtin.lua b/runtime/lua/vim/_meta/builtin.lua index fe45546acd..118799e724 100644 --- a/runtime/lua/vim/_meta/builtin.lua +++ b/runtime/lua/vim/_meta/builtin.lua @@ -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 diff --git a/runtime/lua/vim/diagnostic/_float.lua b/runtime/lua/vim/diagnostic/_float.lua index 2cca7784ba..85da04d2cc 100644 --- a/runtime/lua/vim/diagnostic/_float.lua +++ b/runtime/lua/vim/diagnostic/_float.lua @@ -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( diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index 4cd2c92b60..4f0dbd8664 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -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) diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index 4401541a55..7ae4c5367f 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -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) diff --git a/runtime/lua/vim/health/health.lua b/runtime/lua/vim/health/health.lua index 0bec0220c6..1f666eb48d 100644 --- a/runtime/lua/vim/health/health.lua +++ b/runtime/lua/vim/health/health.lua @@ -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. diff --git a/runtime/lua/vim/loader.lua b/runtime/lua/vim/loader.lua index 9123a68a77..a445ed2894 100644 --- a/runtime/lua/vim/loader.lua +++ b/runtime/lua/vim/loader.lua @@ -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 diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index ca74350b05..dd00534199 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -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, { diff --git a/runtime/lua/vim/lsp/_changetracking.lua b/runtime/lua/vim/lsp/_changetracking.lua index f68f8dd505..defa70b382 100644 --- a/runtime/lua/vim/lsp/_changetracking.lua +++ b/runtime/lua/vim/lsp/_changetracking.lua @@ -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', { diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index 465f75a7a7..230bdcb115 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -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], } diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 5f38ddab59..3410a184fd 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -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), }, diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index 7e2bba393a..8da7664cd3 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -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) diff --git a/runtime/lua/vim/lsp/document_color.lua b/runtime/lua/vim/lsp/document_color.lua index e4a455bb4a..a15dc5f356 100644 --- a/runtime/lua/vim/lsp/document_color.lua +++ b/runtime/lua/vim/lsp/document_color.lua @@ -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 diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index a3aeb3a5ba..d006cf678c 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -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, diff --git a/runtime/lua/vim/lsp/tagfunc.lua b/runtime/lua/vim/lsp/tagfunc.lua index 35c1031b41..ea3f8a38e8 100644 --- a/runtime/lua/vim/lsp/tagfunc.lua +++ b/runtime/lua/vim/lsp/tagfunc.lua @@ -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 diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index d35a3b2c18..fb48bab6f4 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -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 diff --git a/runtime/lua/vim/pack/_lsp.lua b/runtime/lua/vim/pack/_lsp.lua index e42149b7bb..1e702b05e0 100644 --- a/runtime/lua/vim/pack/_lsp.lua +++ b/runtime/lua/vim/pack/_lsp.lua @@ -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 diff --git a/runtime/lua/vim/pos/_util.lua b/runtime/lua/vim/pos/_util.lua index be43d2b83e..8123c8fd58 100644 --- a/runtime/lua/vim/pos/_util.lua +++ b/runtime/lua/vim/pos/_util.lua @@ -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 diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua index 6d98b08dc4..8fe7af3c74 100644 --- a/runtime/lua/vim/ui.lua +++ b/runtime/lua/vim/ui.lua @@ -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 diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index bfc0c342cb..2bf404f11d 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -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 diff --git a/runtime/plugin/zip.lua b/runtime/plugin/zip.lua index dc3f09c9c9..07f8fbb147 100644 --- a/runtime/plugin/zip.lua +++ b/runtime/plugin/zip.lua @@ -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) diff --git a/src/gen/gen_steps.zig b/src/gen/gen_steps.zig index f1a7758156..0f39365274 100644 --- a/src/gen/gen_steps.zig +++ b/src/gen/gen_steps.zig @@ -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}))); diff --git a/src/gen/gen_vimdoc.lua b/src/gen/gen_vimdoc.lua index 87523c3034..aa1cf1448d 100755 --- a/src/gen/gen_vimdoc.lua +++ b/src/gen/gen_vimdoc.lua @@ -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 diff --git a/src/gen/lint.lua b/src/gen/lint.lua index 1c70091b5f..7f0c3c38b8 100644 --- a/src/gen/lint.lua +++ b/src/gen/lint.lua @@ -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, diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index d4bf2f0487..96a53bebf5 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -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 ) diff --git a/src/nvim/lua/stdlib.c b/src/nvim/lua/stdlib.c index f1b49e9fd7..4e3ba95ac3 100644 --- a/src/nvim/lua/stdlib.c +++ b/src/nvim/lua/stdlib.c @@ -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); diff --git a/src/nvim/path.c b/src/nvim/path.c index fdf1e7e7d4..f67a1504ca 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -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); } diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua index 075fc24a5d..99ca1dea23 100644 --- a/test/functional/core/main_spec.lua +++ b/test/functional/core/main_spec.lua @@ -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({ diff --git a/test/functional/lua/diagnostic_spec.lua b/test/functional/lua/diagnostic_spec.lua index 5c85376542..f6b1ae84d5 100644 --- a/test/functional/lua/diagnostic_spec.lua +++ b/test/functional/lua/diagnostic_spec.lua @@ -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,', diff --git a/test/functional/lua/filetype_spec.lua b/test/functional/lua/filetype_spec.lua index fdc6e8c757..4a4505878d 100644 --- a/test/functional/lua/filetype_spec.lua +++ b/test/functional/lua/filetype_spec.lua @@ -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) diff --git a/test/functional/lua/fs_spec.lua b/test/functional/lua/fs_spec.lua index a6608ec278..625cd4df34 100644 --- a/test/functional/lua/fs_spec.lua +++ b/test/functional/lua/fs_spec.lua @@ -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() diff --git a/test/functional/lua/thread_spec.lua b/test/functional/lua/thread_spec.lua index 814c1bb611..ac88fe1f81 100644 --- a/test/functional/lua/thread_spec.lua +++ b/test/functional/lua/thread_spec.lua @@ -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) diff --git a/test/functional/lua/ui_spec.lua b/test/functional/lua/ui_spec.lua index 6dd4bba060..3f68570054 100644 --- a/test/functional/lua/ui_spec.lua +++ b/test/functional/lua/ui_spec.lua @@ -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) diff --git a/test/functional/lua/uri_spec.lua b/test/functional/lua/uri_spec.lua index 9cd8936b17..1bc17222e3 100644 --- a/test/functional/lua/uri_spec.lua +++ b/test/functional/lua/uri_spec.lua @@ -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) diff --git a/test/functional/plugin/lsp/buf_spec.lua b/test/functional/plugin/lsp/buf_spec.lua index efd4794046..258c397615 100644 --- a/test/functional/plugin/lsp/buf_spec.lua +++ b/test/functional/plugin/lsp/buf_spec.lua @@ -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) diff --git a/test/functional/plugin/lsp/diagnostic_spec.lua b/test/functional/plugin/lsp/diagnostic_spec.lua index df530ed472..3d7eb46cab 100644 --- a/test/functional/plugin/lsp/diagnostic_spec.lua +++ b/test/functional/plugin/lsp/diagnostic_spec.lua @@ -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) diff --git a/test/functional/plugin/lsp/util_spec.lua b/test/functional/plugin/lsp/util_spec.lua index 4904bde9aa..169cfd3e60 100644 --- a/test/functional/plugin/lsp/util_spec.lua +++ b/test/functional/plugin/lsp/util_spec.lua @@ -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) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 2161199f4c..b921de93f3 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -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) diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua index bbe0b2f26a..c294381e66 100644 --- a/test/functional/plugin/pack_spec.lua +++ b/test/functional/plugin/pack_spec.lua @@ -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 = { diff --git a/test/functional/plugin/zip_spec.lua b/test/functional/plugin/zip_spec.lua index 95b760fb05..219e2c80eb 100644 --- a/test/functional/plugin/zip_spec.lua +++ b/test/functional/plugin/zip_spec.lua @@ -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() diff --git a/test/old/testdir/test_gf.vim b/test/old/testdir/test_gf.vim index c82dc1705c..17b79080ff 100644 --- a/test/old/testdir/test_gf.vim +++ b/test/old/testdir/test_gf.vim @@ -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("")) + " Nvim: Backslashes "\" not supported in URLs, thus "URL:" is ignored. + call assert_equal("\\\\machine.name\\vimtest2c", expand("")) call search("^fourth") call search("URL") - call assert_equal("URL:\\\\machine.name\\tmp\\vimtest2d", expand("")) + call assert_equal("URL", expand("")) call search("^fifth") call search("URL") diff --git a/test/unit/path_spec.lua b/test/unit/path_spec.lua index 5d9edc8d9e..eb9d50bc57 100644 --- a/test/unit/path_spec.lua +++ b/test/unit/path_spec.lua @@ -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)