aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/lua.txt24
-rw-r--r--runtime/doc/news.txt9
-rw-r--r--runtime/doc/various.txt9
-rw-r--r--runtime/lua/vim/lsp/handlers.lua16
-rw-r--r--runtime/lua/vim/ui.lua50
-rw-r--r--runtime/plugin/nvim.lua28
-rw-r--r--src/nvim/plines.c22
-rw-r--r--test/functional/legacy/scroll_opt_spec.lua40
-rw-r--r--test/functional/lua/ui_spec.lua23
9 files changed, 192 insertions, 29 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..e3d4625c3b 100644
--- a/runtime/plugin/nvim.lua
+++ b/runtime/plugin/nvim.lua
@@ -18,3 +18,31 @@ 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
+if vim.fn.maparg('gx', 'n') == '' then
+ vim.keymap.set({ 'n' }, 'gx', function()
+ do_open(vim.fn.expand('<cfile>'))
+ end, { desc = gx_desc })
+end
+if vim.fn.maparg('gx', 'x') == '' then
+ vim.keymap.set({ 'x' }, 'gx', function()
+ do_open(get_visual_selection())
+ end, { desc = gx_desc })
+end
diff --git a/src/nvim/plines.c b/src/nvim/plines.c
index 46bbbeeb07..615ce9100b 100644
--- a/src/nvim/plines.c
+++ b/src/nvim/plines.c
@@ -98,19 +98,15 @@ int plines_win_nofill(win_T *wp, linenr_T lnum, bool winheight)
/// "wp". Does not care about folding, 'wrap' or 'diff'.
int plines_win_nofold(win_T *wp, linenr_T lnum)
{
- char *s;
- unsigned col;
- int width;
+ char *s = ml_get_buf(wp->w_buffer, lnum, false);
chartabsize_T cts;
-
- s = ml_get_buf(wp->w_buffer, lnum, false);
init_chartabsize_arg(&cts, wp, lnum, 0, s, s);
if (*s == NUL && !cts.cts_has_virt_text) {
return 1; // be quick for an empty line
}
win_linetabsize_cts(&cts, (colnr_T)MAXCOL);
clear_chartabsize_arg(&cts);
- col = (unsigned)cts.cts_vcol;
+ int64_t col = cts.cts_vcol;
// If list mode is on, then the '$' at the end of the line may take up one
// extra column.
@@ -119,17 +115,17 @@ int plines_win_nofold(win_T *wp, linenr_T lnum)
}
// Add column offset for 'number', 'relativenumber' and 'foldcolumn'.
- width = wp->w_width_inner - win_col_off(wp);
- if (width <= 0 || col > 32000) {
- return 32000; // bigger than the number of screen columns
+ int width = wp->w_width_inner - win_col_off(wp);
+ if (width <= 0) {
+ return 32000; // bigger than the number of screen lines
}
- if (col <= (unsigned)width) {
+ if (col <= width) {
return 1;
}
- col -= (unsigned)width;
+ col -= width;
width += win_col_off2(wp);
- assert(col <= INT_MAX && (int)col < INT_MAX - (width - 1));
- return ((int)col + (width - 1)) / width + 1;
+ const int64_t lines = (col + (width - 1)) / width + 1;
+ return (lines > 0 && lines <= INT_MAX) ? (int)lines : INT_MAX;
}
/// Like plines_win(), but only reports the number of physical screen lines
diff --git a/test/functional/legacy/scroll_opt_spec.lua b/test/functional/legacy/scroll_opt_spec.lua
index ae6a409762..b179338665 100644
--- a/test/functional/legacy/scroll_opt_spec.lua
+++ b/test/functional/legacy/scroll_opt_spec.lua
@@ -1109,4 +1109,44 @@ describe('smoothscroll', function()
|
]])
end)
+
+ it('works with very long line', function()
+ screen:set_default_attr_ids({
+ [1] = {foreground = Screen.colors.Brown},
+ [2] = {foreground = Screen.colors.Blue1, bold = true},
+ })
+ exec([[
+ edit test/functional/fixtures/bigfile_oneline.txt
+ setlocal smoothscroll number
+ ]])
+ screen:expect([[
+ {1: 1 }^0000;<control>;Cc;0;BN;;;;;N;NULL;;;|
+ {1: }; 0001;<control>;Cc;0;BN;;;;;N;START|
+ {1: } OF HEADING;;;; 0002;<control>;Cc;0;|
+ {1: }BN;;;;;N;START OF TEXT;;;; 0003;<con|
+ {1: }trol>;Cc;0;BN;;;;;N;END OF TEXT;;;; |
+ {1: }0004;<control>;Cc;0;BN;;;;;N;END OF |
+ {1: }TRANSMISSION;;;; 0005;<control>;Cc;0|
+ {1: };BN;;;;;N;ENQUIRY;;;; 0006;<control>|
+ {1: };Cc;0;BN;;;;;N;ACKNOWLEDGE;;;; 0007;|
+ {1: }<control>;Cc;0;BN;;;;;N;BELL;;;; 000|
+ {1: }8;<control>;Cc;0;BN;;;;;N;BACKSPACE;|
+ |
+ ]])
+ feed('j')
+ screen:expect([[
+ {2:<<<}{1: }CJK COMPATIBILITY IDEOGRAPH-2F91F;Lo|
+ {1: };0;L;243AB;;;;N;;;;; 2F920;CJK COMPA|
+ {1: }TIBILITY IDEOGRAPH-2F920;Lo;0;L;7228|
+ {1: };;;;N;;;;; 2F921;CJK COMPATIBILITY I|
+ {1: }DEOGRAPH-2F921;Lo;0;L;7235;;;;N;;;;;|
+ {1: } 2F922;CJK COMPATIBILITY IDEOGRAPH-2|
+ {1: }F922;Lo;0;L;7250;;;;N;;;;; |
+ {1: 2 }^2F923;CJK COMPATIBILITY IDEOGRAPH-2F|
+ {1: }923;Lo;0;L;24608;;;;N;;;;; |
+ {1: 3 }2F924;CJK COMPATIBILITY IDEOGRAPH-2F|
+ {1: }924;Lo;0;L;7280;;;;N;;;;; |
+ |
+ ]])
+ end)
end)
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)