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 | 28 | ||||
-rw-r--r-- | src/nvim/plines.c | 22 | ||||
-rw-r--r-- | test/functional/legacy/scroll_opt_spec.lua | 40 | ||||
-rw-r--r-- | test/functional/lua/ui_spec.lua | 23 |
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) |