diff options
-rw-r--r-- | runtime/doc/lua.txt | 24 | ||||
-rw-r--r-- | runtime/doc/news.txt | 9 | ||||
-rw-r--r-- | runtime/doc/various.txt | 9 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 16 | ||||
-rw-r--r-- | runtime/lua/vim/ui.lua | 50 | ||||
-rw-r--r-- | runtime/plugin/nvim.lua | 24 | ||||
-rw-r--r-- | test/functional/lua/ui_spec.lua | 23 |
7 files changed, 139 insertions, 16 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index fb6cbca6e3..77a89a123d 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -2343,6 +2343,30 @@ input({opts}, {on_confirm}) *vim.ui.input()* typed (it might be an empty string if nothing was entered), or `nil` if the user aborted the dialog. +open({path}) *vim.ui.open()* + Opens `path` with the system default handler (macOS `open`, Windows + `explorer.exe`, Linux `xdg-open`, …), or returns (but does not show) an + error message on failure. + + Expands "~/" and environment variables in filesystem paths. + + Examples: >lua + + vim.ui.open("https://neovim.io/") + vim.ui.open("~/path/to/file") + vim.ui.open("$VIMRUNTIME") +< + + Parameters: ~ + • {path} (string) Path or URL to open + + Return: ~ + SystemCompleted|nil # Command result, or nil if not found. + (string|nil) # Error message on failure + + See also: ~ + • |vim.system()| + select({items}, {opts}, {on_choice}) *vim.ui.select()* Prompts the user to pick from a list of items, allowing arbitrary (potentially asynchronous) work until `on_choice`. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 61ae92296f..24e9dc917b 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -105,7 +105,10 @@ The following new APIs and features were added. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint • Bundled treesitter parser and queries (highlight, folds) for Markdown, -Python, and Bash. + Python, and Bash. + +• |vim.ui.open()| opens URIs using the system default handler (macOS `open`, + Windows `explorer`, Linux `xdg-open`, etc.) ============================================================================== CHANGED FEATURES *news-changed* @@ -143,6 +146,10 @@ The following changes to existing APIs or features add new behavior. • |:Man| now respects 'wrapmargin' +• |gx| now uses |vim.ui.open()| and not netrw. To customize, you can redefine + `vim.ui.open` or remap `gx`. To continue using netrw (deprecated): >vim + :call netrw#BrowseX(expand(exists("g:netrw_gx")? g:netrw_gx : '<cfile>'), netrw#CheckIfRemote())<CR> + ============================================================================== REMOVED FEATURES *news-removed* diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt index 1b1dca321b..956c37fc0f 100644 --- a/runtime/doc/various.txt +++ b/runtime/doc/various.txt @@ -97,6 +97,15 @@ g8 Print the hex values of the bytes used in the cursor is halfway through a multibyte character the command won't move the cursor. + *gx* +gx Opens the current filepath or URL (decided by + |<cfile>|, 'isfname') at cursor using the system + default handler, by calling |vim.ui.open()|. + + *v_gx* +{Visual}gx Opens the selected text using the system default + handler, by calling |vim.ui.open()|. + *:p* *:pr* *:print* *E749* :[range]p[rint] [flags] Print [range] lines (default current line). diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 625a2ed282..70781cb7a6 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -573,22 +573,14 @@ M['window/showDocument'] = function(_, result, ctx, _) if result.external then -- TODO(lvimuser): ask the user for confirmation - local cmd - if vim.fn.has('win32') == 1 then - cmd = { 'cmd.exe', '/c', 'start', '""', uri } - elseif vim.fn.has('macunix') == 1 then - cmd = { 'open', uri } - else - cmd = { 'xdg-open', uri } - end + local ret, err = vim.ui.open(uri) - local ret = vim.fn.system(cmd) - if vim.v.shell_error ~= 0 then + if ret == nil or ret.code ~= 0 then return { success = false, error = { code = protocol.ErrorCodes.UnknownErrorCode, - message = ret, + message = ret and ret.stderr or err, }, } end @@ -600,7 +592,7 @@ M['window/showDocument'] = function(_, result, ctx, _) local client = vim.lsp.get_client_by_id(client_id) local client_name = client and client.name or string.format('id=%d', client_id) if not client then - err_message({ 'LSP[', client_name, '] client has shut down after sending ', ctx.method }) + err_message('LSP[', client_name, '] client has shut down after sending ', ctx.method) return vim.NIL end diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua index aaee175f3a..fd06611da2 100644 --- a/runtime/lua/vim/ui.lua +++ b/runtime/lua/vim/ui.lua @@ -104,4 +104,54 @@ function M.input(opts, on_confirm) end end +--- Opens `path` with the system default handler (macOS `open`, Windows `explorer.exe`, Linux +--- `xdg-open`, …), or returns (but does not show) an error message on failure. +--- +--- Expands "~/" and environment variables in filesystem paths. +--- +--- Examples: +--- <pre>lua +--- vim.ui.open("https://neovim.io/") +--- vim.ui.open("~/path/to/file") +--- vim.ui.open("$VIMRUNTIME") +--- </pre> +--- +---@param path string Path or URL to open +--- +---@return SystemCompleted|nil # Command result, or nil if not found. +---@return string|nil # Error message on failure +--- +---@see |vim.system()| +function M.open(path) + vim.validate({ + path = { path, 'string' }, + }) + local is_uri = path:match('%w+:') + if not is_uri then + path = vim.fn.expand(path) + end + + local cmd + + if vim.fn.has('mac') == 1 then + cmd = { 'open', path } + elseif vim.fn.has('win32') == 1 then + cmd = { 'explorer', path } + elseif vim.fn.executable('wslview') == 1 then + cmd = { 'wslview', path } + elseif vim.fn.executable('xdg-open') == 1 then + cmd = { 'xdg-open', path } + else + return nil, 'vim.ui.open: no handler found (tried: wslview, xdg-open)' + end + + local rv = vim.system(cmd, { text = true, detach = true }):wait() + if rv.code ~= 0 then + local msg = ('vim.ui.open: command failed (%d): %s'):format(rv.code, vim.inspect(cmd)) + return rv, msg + end + + return rv, nil +end + return M diff --git a/runtime/plugin/nvim.lua b/runtime/plugin/nvim.lua index 0a33826b82..33d399e577 100644 --- a/runtime/plugin/nvim.lua +++ b/runtime/plugin/nvim.lua @@ -18,3 +18,27 @@ vim.api.nvim_create_user_command('InspectTree', function(cmd) vim.treesitter.inspect_tree() end end, { desc = 'Inspect treesitter language tree for buffer', count = true }) + +-- TODO: use vim.region() when it lands... #13896 #16843 +local function get_visual_selection() + local save_a = vim.fn.getreginfo('a') + vim.cmd([[norm! "ay]]) + local selection = vim.fn.getreg('a', 1) + vim.fn.setreg('a', save_a) + return selection +end + +local gx_desc = + 'Opens filepath or URI under cursor with the system handler (file explorer, web browser, …)' +local function do_open(uri) + local _, err = vim.ui.open(uri) + if err then + vim.notify(err, vim.log.levels.ERROR) + end +end +vim.keymap.set({ 'n' }, 'gx', function() + do_open(vim.fn.expand('<cfile>')) +end, { desc = gx_desc }) +vim.keymap.set({ 'x' }, 'gx', function() + do_open(get_visual_selection()) +end, { desc = gx_desc }) diff --git a/test/functional/lua/ui_spec.lua b/test/functional/lua/ui_spec.lua index 9ee99b4905..d1b64419af 100644 --- a/test/functional/lua/ui_spec.lua +++ b/test/functional/lua/ui_spec.lua @@ -1,9 +1,11 @@ local helpers = require('test.functional.helpers')(after_each) local eq = helpers.eq +local matches = helpers.matches local exec_lua = helpers.exec_lua local clear = helpers.clear local feed = helpers.feed local eval = helpers.eval +local is_os = helpers.is_os local poke_eventloop = helpers.poke_eventloop describe('vim.ui', function() @@ -11,8 +13,7 @@ describe('vim.ui', function() clear() end) - - describe('select', function() + describe('select()', function() it('can select an item', function() local result = exec_lua[[ local items = { @@ -47,7 +48,7 @@ describe('vim.ui', function() end) end) - describe('input', function() + describe('input()', function() it('can input text', function() local result = exec_lua[[ local opts = { @@ -130,4 +131,20 @@ describe('vim.ui', function() end) end) + + describe('open()', function() + it('validation', function() + if not is_os('bsd') then + matches('vim.ui.open: command failed %(%d%): { "[^"]+", "non%-existent%-file" }', + exec_lua[[local _, err = vim.ui.open('non-existent-file') ; return err]]) + end + + exec_lua[[ + vim.fn.has = function() return 0 end + vim.fn.executable = function() return 0 end + ]] + eq('vim.ui.open: no handler found (tried: wslview, xdg-open)', + exec_lua[[local _, err = vim.ui.open('foo') ; return err]]) + end) + end) end) |