aboutsummaryrefslogtreecommitdiff
path: root/runtime
diff options
context:
space:
mode:
Diffstat (limited to 'runtime')
-rw-r--r--runtime/doc/api.txt8
-rw-r--r--runtime/doc/autocmd.txt4
-rw-r--r--runtime/doc/change.txt6
-rw-r--r--runtime/doc/cmdline.txt1
-rw-r--r--runtime/doc/eval.txt8
-rw-r--r--runtime/doc/lua.txt32
-rw-r--r--runtime/doc/mbyte.txt3
-rw-r--r--runtime/doc/news.txt20
-rw-r--r--runtime/doc/provider.txt31
-rw-r--r--runtime/doc/vim_diff.txt13
-rw-r--r--runtime/lua/vim/_defaults.lua15
-rw-r--r--runtime/lua/vim/_init_packages.lua1
-rw-r--r--runtime/lua/vim/_meta/api.lua7
-rw-r--r--runtime/lua/vim/_meta/options.lua2
-rw-r--r--runtime/lua/vim/snippet.lua72
-rw-r--r--runtime/lua/vim/termcap.lua60
-rw-r--r--runtime/lua/vim/text.lua32
-rw-r--r--runtime/lua/vim/ui/clipboard/osc52.lua107
-rw-r--r--runtime/plugin/osc52.lua36
19 files changed, 347 insertions, 111 deletions
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt
index 1b11d8571a..57491b34d6 100644
--- a/runtime/doc/api.txt
+++ b/runtime/doc/api.txt
@@ -3598,13 +3598,13 @@ nvim_ui_set_option({name}, {value}) *nvim_ui_set_option()*
|RPC| only
nvim_ui_term_event({event}, {value}) *nvim_ui_term_event()*
- Tells Nvim when a terminal event has occurred: sets |v:termresponse| and
- fires |TermResponse|.
+ Tells Nvim when a terminal event has occurred
The following terminal events are supported:
- • "osc_response": The terminal sent a OSC response sequence to Nvim. The
- payload is the received OSC sequence.
+ • "termresponse": The terminal sent an OSC or DCS response sequence to
+ Nvim. The payload is the received response. Sets |v:termresponse| and
+ fires |TermResponse|.
Attributes: ~
|RPC| only
diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt
index 6b698b0868..c6f6559e37 100644
--- a/runtime/doc/autocmd.txt
+++ b/runtime/doc/autocmd.txt
@@ -987,8 +987,8 @@ TermClose When a |terminal| job ends.
Sets these |v:event| keys:
status
*TermResponse*
-TermResponse When Nvim receives a OSC response from the
- terminal. Sets |v:termresponse|. When used
+TermResponse When Nvim receives an OSC or DCS response from
+ the terminal. Sets |v:termresponse|. When used
from Lua, the response string is included in
the "data" field of the autocommand callback.
May be triggered halfway through another event
diff --git a/runtime/doc/change.txt b/runtime/doc/change.txt
index 2c47421b02..e1bb7c5fc7 100644
--- a/runtime/doc/change.txt
+++ b/runtime/doc/change.txt
@@ -619,9 +619,9 @@ original user.
current line only. When [count] is given, replace in
[count] lines, starting with the last line in [range].
When [range] is omitted start in the current line.
- *E939*
- [count] must be a positive number. Also see
- |cmdline-ranges|.
+ *E939* *E1510*
+ [count] must be a positive number (max 2147483647)
+ Also see |cmdline-ranges|.
See |:s_flags| for [flags].
The delimiter doesn't need to be /, see
diff --git a/runtime/doc/cmdline.txt b/runtime/doc/cmdline.txt
index 291286d8f7..8bed8a9ffc 100644
--- a/runtime/doc/cmdline.txt
+++ b/runtime/doc/cmdline.txt
@@ -339,6 +339,7 @@ terminals)
A positive number represents the absolute index of an entry
as it is given in the first column of a :history listing.
This number remains fixed even if other entries are deleted.
+ (see |E1510|)
A negative number means the relative position of an entry,
counted from the newest entry (which has index -1) backwards.
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index 2223829548..a73932be00 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -2318,10 +2318,10 @@ v:t_string Value of |String| type. Read-only. See: |type()|
v:t_blob Value of |Blob| type. Read-only. See: |type()|
*v:termresponse* *termresponse-variable*
-v:termresponse The value of the most recent OSC escape sequence received by
- Nvim from the terminal. This can be read in a |TermResponse|
- event handler after querying the terminal using another escape
- sequence.
+v:termresponse The value of the most recent OSC or DCS escape sequence
+ received by Nvim from the terminal. This can be read in a
+ |TermResponse| event handler after querying the terminal using
+ another escape sequence.
*v:testing* *testing-variable*
v:testing Must be set before using `test_garbagecollect_now()`.
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
index ada07a71f8..596b58d4ff 100644
--- a/runtime/doc/lua.txt
+++ b/runtime/doc/lua.txt
@@ -3697,16 +3697,16 @@ totable({f}, {...}) *vim.iter.totable()*
==============================================================================
Lua module: vim.snippet *vim.snippet*
-active() *snippet.active()*
+vim.snippet.active() *vim.snippet.active()*
Returns `true` if there's an active snippet in the current buffer.
Return: ~
(boolean)
-exit() *snippet.exit()*
+vim.snippet.exit() *vim.snippet.exit()*
Exits the current snippet.
-expand({input}) *snippet.expand()*
+vim.snippet.expand({input}) *vim.snippet.expand()*
Expands the given snippet text. Refer to https://microsoft.github.io/language-server-protocol/specification/#snippet_syntax for the specification of valid input.
Tabstops are highlighted with hl-SnippetTabstop.
@@ -3714,7 +3714,7 @@ expand({input}) *snippet.expand()*
Parameters: ~
• {input} (string)
-jump({direction}) *snippet.jump()*
+vim.snippet.jump({direction}) *vim.snippet.jump()*
Jumps within the active snippet in the given direction. If the jump isn't
possible, the function call does nothing.
@@ -3732,7 +3732,7 @@ jump({direction}) *snippet.jump()*
• {direction} (vim.snippet.Direction) Navigation direction. -1 for
previous, 1 for next.
-jumpable({direction}) *snippet.jumpable()*
+vim.snippet.jumpable({direction}) *vim.snippet.jumpable()*
Returns `true` if there is an active snippet which can be jumped in the
given direction. You can use this function to navigate a snippet as
follows: >lua
@@ -3752,4 +3752,26 @@ jumpable({direction}) *snippet.jumpable()*
Return: ~
(boolean)
+
+==============================================================================
+Lua module: vim.text *vim.text*
+
+vim.text.hexdecode({enc}) *vim.text.hexdecode()*
+ Hex decode a string.
+
+ Parameters: ~
+ • {enc} (string) String to decode
+
+ Return: ~
+ (string) Decoded string
+
+vim.text.hexencode({str}) *vim.text.hexencode()*
+ Hex encode a string.
+
+ Parameters: ~
+ • {str} (string) String to encode
+
+ Return: ~
+ (string) Hex encoded string
+
vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl:
diff --git a/runtime/doc/mbyte.txt b/runtime/doc/mbyte.txt
index aedef87a09..0a7e0baad3 100644
--- a/runtime/doc/mbyte.txt
+++ b/runtime/doc/mbyte.txt
@@ -646,7 +646,8 @@ widespread as file format.
A composing or combining character is used to change the meaning of the
character before it. The combining characters are drawn on top of the
preceding character.
-Up to six combining characters can be displayed.
+Too big combined characters cannot be displayed, but they can still be
+inspected using the |g8| and |ga| commands described below.
When editing text a composing character is mostly considered part of the
preceding character. For example "x" will delete a character and its
following composing characters by default.
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 818f0e5a57..8073a4f162 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -15,6 +15,13 @@ BREAKING CHANGES *news-breaking*
The following changes may require adaptations in user config or plugins.
+• In some cases, the cursor in the Nvim |TUI| would blink even without
+ configuring 'guicursor' as mentioned in |cursor-blinking|. This was a bug
+ that has now been fixed. If your cursor has stopped blinking, add the
+ following (or similar, adapted to user preference) to your |config| file: >vim
+
+ set guicursor+=n-v-c:blinkon500-blinkoff500
+<
• |vim.tbl_islist()| now checks whether a table is actually list-like (i.e.,
has integer keys without gaps and starting from 1). For the previous
behavior (only check for integer keys, allow gaps or not starting with 1),
@@ -217,11 +224,15 @@ The following new APIs and features were added.
read escape sequence responses from the terminal.
• A clipboard provider which uses OSC 52 to copy the selection to the system
- clipboard is now bundled by default. |clipboard-osc52|
+ clipboard is now bundled by default and will be automatically enabled under
+ certain conditions. |clipboard-osc52|
• The 'termsync' option asks the terminal emulator to buffer screen updates
until the redraw cycle is complete. Requires support from the terminal.
+• Added |vim.text.hexencode()| and |vim.text.hexdecode()| to convert strings
+ to and from byte representations.
+
==============================================================================
CHANGED FEATURES *news-changed*
@@ -298,6 +309,13 @@ The following changes to existing APIs or features add new behavior.
Note that syntax highlighting of code examples requires a matching parser
and may be affected by custom queries.
+• Support for rendering multibyte characters using composing characters has been
+ enhanced. The maximum limit have been increased from 1+6 codepoints to
+ 31 bytes, which is guaranteed to fit all chars from before but often more.
+
+ NOTE: the regexp engine still has a hard-coded limit of considering
+ 6 composing chars only.
+
==============================================================================
REMOVED FEATURES *news-removed*
diff --git a/runtime/doc/provider.txt b/runtime/doc/provider.txt
index 23bde05072..b8182347f8 100644
--- a/runtime/doc/provider.txt
+++ b/runtime/doc/provider.txt
@@ -260,27 +260,40 @@ using OSC 52. OSC 52 is an Operating System Command control sequence that
writes the copied text to the terminal emulator. If the terminal emulator
supports OSC 52 then it will write the copied text into the system clipboard.
-This is most useful when using Nvim remotely (e.g. via ssh) as Nvim does not
-have direct access to the system clipboard in that case.
+Nvim will attempt to automatically determine if the host terminal emulator
+supports the OSC 52 sequence and enable the OSC 52 clipboard provider if it
+does as long as all of the following are true:
-Because not all terminal emulators support OSC 52, this provider must be opted
-into explicitly by setting the following |g:clipboard| definition: >lua
+ • Nvim is running in the |TUI|
+ • |g:clipboard| is unset
+ • 'clipboard' is not set to "unnamed" or "unnamedplus"
+ • $SSH_TTY is set
+
+If any of the above conditions are not met then the OSC 52 clipboard provider
+will not be used by default and Nvim will fall back to discovering a
+|clipboard-tool| through the usual process.
+
+To force Nvim to use the OSC 52 provider you can use the following
+|g:clipboard| definition: >lua
vim.g.clipboard = {
name = 'OSC 52',
copy = {
- ['+'] = require('vim.ui.clipboard.osc52').copy,
- ['*'] = require('vim.ui.clipboard.osc52').copy,
+ ['+'] = require('vim.ui.clipboard.osc52').copy('+'),
+ ['*'] = require('vim.ui.clipboard.osc52').copy('*'),
},
paste = {
- ['+'] = require('vim.ui.clipboard.osc52').paste,
- ['*'] = require('vim.ui.clipboard.osc52').paste,
+ ['+'] = require('vim.ui.clipboard.osc52').paste('+'),
+ ['*'] = require('vim.ui.clipboard.osc52').paste('*'),
},
}
<
Note that not all terminal emulators support reading from the system clipboard
(and even for those that do, users should be aware of the security
-implications), so using OSC 52 for pasting may not be possible.
+implications), so using OSC 52 for pasting may not be possible (and not
+necessary, because you can |paste| instead using your system paste function).
+Users may need to configure their terminal emulator to allow reading from the
+clipboard.
<
==============================================================================
Paste *provider-paste* *paste*
diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt
index fb23535c76..cf9b3cf0e5 100644
--- a/runtime/doc/vim_diff.txt
+++ b/runtime/doc/vim_diff.txt
@@ -723,9 +723,16 @@ Options:
<
*'macatsui'*
*'maxcombine'* *'mco'*
- Nvim always displays up to 6 combining characters. You can still edit
- text with more than 6 combining characters, you just can't see them.
- Use |g8| or |ga|. See |mbyte-combining|.
+ Nvim counts maximum character sizes in bytes, not codepoints. This is
+ guaranteed to be big enough to always fit all chars properly displayed
+ in vim with 'maxcombine' set to 6.
+
+ You can still edit text with larger characters than fits in the screen buffer,
+ you just can't see them. Use |g8| or |ga|. See |mbyte-combining|.
+
+ NOTE: the rexexp engine still has a hard-coded limit of considering
+ 6 composing chars only.
+
*'maxmem'* Nvim delegates memory-management to the OS.
*'maxmemtot'* Nvim delegates memory-management to the OS.
printoptions
diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua
index 870603c9f3..09d6d43e7a 100644
--- a/runtime/lua/vim/_defaults.lua
+++ b/runtime/lua/vim/_defaults.lua
@@ -190,14 +190,17 @@ do
--- @param c string Color as a string of hex chars
--- @return number? Intensity of the color
local function parsecolor(c)
- local len = #c
- assert(len > 0 and len <= 4, 'Invalid hex color string')
- if not c:match('^0x') then
- c = string.format('0x%s', c)
+ if #c == 0 or #c > 4 then
+ return nil
end
- local max = tonumber(string.format('0x%s', string.rep('f', len)))
- return tonumber(c) / max
+ local val = tonumber(c, 16)
+ if not val then
+ return nil
+ end
+
+ local max = tonumber(string.rep('f', #c), 16)
+ return val / max
end
--- Parse an OSC 11 response
diff --git a/runtime/lua/vim/_init_packages.lua b/runtime/lua/vim/_init_packages.lua
index 8750afba34..4a961970cc 100644
--- a/runtime/lua/vim/_init_packages.lua
+++ b/runtime/lua/vim/_init_packages.lua
@@ -57,6 +57,7 @@ vim._submodules = {
fs = true,
iter = true,
re = true,
+ text = true,
}
-- These are for loading runtime modules in the vim namespace lazily.
diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua
index 70a8b0aec2..006996ad4e 100644
--- a/runtime/lua/vim/_meta/api.lua
+++ b/runtime/lua/vim/_meta/api.lua
@@ -2065,11 +2065,12 @@ function vim.api.nvim_ui_set_focus(gained) end
--- @param value any
function vim.api.nvim_ui_set_option(name, value) end
---- Tells Nvim when a terminal event has occurred.
+--- Tells Nvim when a terminal event has occurred
--- The following terminal events are supported:
---
---- • "osc_response": The terminal sent a OSC response sequence to Nvim. The
---- payload is the received OSC sequence.
+--- • "termresponse": The terminal sent an OSC or DCS response sequence to
+--- Nvim. The payload is the received response. Sets `v:termresponse` and
+--- fires `TermResponse`.
---
---
--- @param event string Event name
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
index 6d693ca036..d2bdab4d28 100644
--- a/runtime/lua/vim/_meta/options.lua
+++ b/runtime/lua/vim/_meta/options.lua
@@ -2576,7 +2576,7 @@ vim.go.fp = vim.go.formatprg
--- security reasons.
---
--- @type boolean
-vim.o.fsync = false
+vim.o.fsync = true
vim.o.fs = vim.o.fsync
vim.go.fsync = vim.o.fsync
vim.go.fs = vim.go.fsync
diff --git a/runtime/lua/vim/snippet.lua b/runtime/lua/vim/snippet.lua
index 94c69795a4..32a8ea0b0d 100644
--- a/runtime/lua/vim/snippet.lua
+++ b/runtime/lua/vim/snippet.lua
@@ -104,8 +104,9 @@ end
--- @class vim.snippet.Tabstop
--- @field extmark_id integer
---- @field index integer
--- @field bufnr integer
+--- @field index integer
+--- @field choices? string[]
local Tabstop = {}
--- Creates a new tabstop.
@@ -114,8 +115,9 @@ local Tabstop = {}
--- @param index integer
--- @param bufnr integer
--- @param range Range4
+--- @param choices? string[]
--- @return vim.snippet.Tabstop
-function Tabstop.new(index, bufnr, range)
+function Tabstop.new(index, bufnr, range, choices)
local extmark_id = vim.api.nvim_buf_set_extmark(bufnr, snippet_ns, range[1], range[2], {
right_gravity = false,
end_right_gravity = true,
@@ -125,7 +127,7 @@ function Tabstop.new(index, bufnr, range)
})
local self = setmetatable(
- { index = index, bufnr = bufnr, extmark_id = extmark_id },
+ { extmark_id = extmark_id, bufnr = bufnr, index = index, choices = choices },
{ __index = Tabstop }
)
@@ -173,9 +175,9 @@ local Session = {}
--- @package
--- @param bufnr integer
--- @param snippet_extmark integer
---- @param tabstop_ranges table<integer, Range4[]>
+--- @param tabstop_data table<integer, { range: Range4, choices?: string[] }[]>
--- @return vim.snippet.Session
-function Session.new(bufnr, snippet_extmark, tabstop_ranges)
+function Session.new(bufnr, snippet_extmark, tabstop_data)
local self = setmetatable({
bufnr = bufnr,
extmark_id = snippet_extmark,
@@ -184,10 +186,10 @@ function Session.new(bufnr, snippet_extmark, tabstop_ranges)
}, { __index = Session })
-- Create the tabstops.
- for index, ranges in pairs(tabstop_ranges) do
- for _, range in ipairs(ranges) do
+ for index, ranges in pairs(tabstop_data) do
+ for _, data in ipairs(ranges) do
self.tabstops[index] = self.tabstops[index] or {}
- table.insert(self.tabstops[index], Tabstop.new(index, self.bufnr, range))
+ table.insert(self.tabstops[index], Tabstop.new(index, self.bufnr, data.range, data.choices))
end
end
@@ -222,6 +224,22 @@ end
--- @field private _session? vim.snippet.Session
local M = { session = nil }
+--- Displays the choices for the given tabstop as completion items.
+---
+--- @param tabstop vim.snippet.Tabstop
+local function display_choices(tabstop)
+ assert(tabstop.choices, 'Tabstop has no choices')
+
+ local start_col = tabstop:get_range()[2] + 1
+ local matches = vim.iter.map(function(choice)
+ return { word = choice }
+ end, tabstop.choices)
+
+ vim.defer_fn(function()
+ vim.fn.complete(start_col, matches)
+ end, 100)
+end
+
--- Select the given tabstop range.
---
--- @param tabstop vim.snippet.Tabstop
@@ -246,17 +264,25 @@ local function select_tabstop(tabstop)
local range = tabstop:get_range()
local mode = vim.fn.mode()
+ if vim.fn.pumvisible() ~= 0 then
+ -- Close the choice completion menu if open.
+ vim.fn.complete(vim.fn.col('.'), {})
+ end
+
-- Move the cursor to the start of the tabstop.
vim.api.nvim_win_set_cursor(0, { range[1] + 1, range[2] })
- -- For empty and the final tabstop, start insert mode at the end of the range.
- if tabstop.index == 0 or (range[1] == range[3] and range[2] == range[4]) then
+ -- For empty, choice and the final tabstops, start insert mode at the end of the range.
+ if tabstop.choices or tabstop.index == 0 or (range[1] == range[3] and range[2] == range[4]) then
if mode ~= 'i' then
if mode == 's' then
feedkeys('<Esc>')
end
vim.cmd.startinsert({ bang = range[4] >= #vim.api.nvim_get_current_line() })
end
+ if tabstop.choices then
+ display_choices(tabstop)
+ end
else
-- Else, select the tabstop's text.
if mode ~= 'n' then
@@ -297,7 +323,6 @@ local function setup_autocmds(bufnr)
return true
end
- -- Update the current tabstop to be the one containing the cursor.
for tabstop_index, tabstops in pairs(M._session.tabstops) do
for _, tabstop in ipairs(tabstops) do
local range = tabstop:get_range()
@@ -305,7 +330,6 @@ local function setup_autocmds(bufnr)
(cursor_row > range[1] or (cursor_row == range[1] and cursor_col >= range[2]))
and (cursor_row < range[3] or (cursor_row == range[3] and cursor_col <= range[4]))
then
- M._session.current_tabstop = tabstop
if tabstop_index ~= 0 then
return
end
@@ -377,14 +401,16 @@ function M.expand(input)
end
-- Keep track of tabstop nodes during expansion.
- --- @type table<integer, Range4[]>
- local tabstop_ranges = {}
+ --- @type table<integer, { range: Range4, choices?: string[] }[]>
+ local tabstop_data = {}
--- @param index integer
- --- @param placeholder string?
- local function add_tabstop(index, placeholder)
- tabstop_ranges[index] = tabstop_ranges[index] or {}
- table.insert(tabstop_ranges[index], compute_tabstop_range(snippet_text, placeholder))
+ --- @param placeholder? string
+ --- @param choices? string[]
+ local function add_tabstop(index, placeholder, choices)
+ tabstop_data[index] = tabstop_data[index] or {}
+ local range = compute_tabstop_range(snippet_text, placeholder)
+ table.insert(tabstop_data[index], { range = range, choices = choices })
end
--- Appends the given text to the snippet, taking care of indentation.
@@ -428,7 +454,7 @@ function M.expand(input)
append_to_snippet(value)
elseif type == G.NodeType.Choice then
--- @cast data vim.snippet.ChoiceData
- append_to_snippet(data.values[1])
+ add_tabstop(data.tabstop, nil, data.values)
elseif type == G.NodeType.Variable then
--- @cast data vim.snippet.VariableData
-- Try to get the variable's value.
@@ -436,7 +462,7 @@ function M.expand(input)
if not value then
-- Unknown variable, make this a tabstop and use the variable name as a placeholder.
value = data.name
- local tabstop_indexes = vim.tbl_keys(tabstop_ranges)
+ local tabstop_indexes = vim.tbl_keys(tabstop_data)
local index = math.max(unpack((#tabstop_indexes == 0 and { 0 }) or tabstop_indexes)) + 1
add_tabstop(index, value)
end
@@ -449,8 +475,8 @@ function M.expand(input)
-- $0, which defaults to the end of the snippet, defines the final cursor position.
-- Make sure the snippet has exactly one of these.
- if vim.tbl_contains(vim.tbl_keys(tabstop_ranges), 0) then
- assert(#tabstop_ranges[0] == 1, 'Snippet has multiple $0 tabstops')
+ if vim.tbl_contains(vim.tbl_keys(tabstop_data), 0) then
+ assert(#tabstop_data[0] == 1, 'Snippet has multiple $0 tabstops')
else
add_tabstop(0)
end
@@ -469,7 +495,7 @@ function M.expand(input)
right_gravity = false,
end_right_gravity = true,
})
- M._session = Session.new(bufnr, snippet_extmark, tabstop_ranges)
+ M._session = Session.new(bufnr, snippet_extmark, tabstop_data)
-- Jump to the first tabstop.
M.jump(1)
diff --git a/runtime/lua/vim/termcap.lua b/runtime/lua/vim/termcap.lua
new file mode 100644
index 0000000000..0eefc5eee4
--- /dev/null
+++ b/runtime/lua/vim/termcap.lua
@@ -0,0 +1,60 @@
+local M = {}
+
+--- Query the host terminal emulator for terminfo capabilities.
+---
+--- This function sends the XTGETTCAP DCS sequence to the host terminal emulator asking the terminal
+--- to send us its terminal capabilities. These are strings that are normally taken from a terminfo
+--- file, however an up to date terminfo database is not always available (particularly on remote
+--- machines), and many terminals continue to misidentify themselves or do not provide their own
+--- terminfo file, making the terminfo database unreliable.
+---
+--- Querying the terminal guarantees that we get a truthful answer, but only if the host terminal
+--- emulator supports the XTGETTCAP sequence.
+---
+--- @param caps string|table A terminal capability or list of capabilities to query
+--- @param cb function(cap:string, seq:string) Function to call when a response is received
+function M.query(caps, cb)
+ vim.validate({
+ caps = { caps, { 'string', 'table' } },
+ cb = { cb, 'f' },
+ })
+
+ if type(caps) ~= 'table' then
+ caps = { caps }
+ end
+
+ local count = #caps
+
+ vim.api.nvim_create_autocmd('TermResponse', {
+ callback = function(args)
+ local resp = args.data ---@type string
+ local k, v = resp:match('^\027P1%+r(%x+)=(%x+)$')
+ if k and v then
+ local cap = vim.text.hexdecode(k)
+ local seq =
+ vim.text.hexdecode(v):gsub('\\E', '\027'):gsub('%%p%d', ''):gsub('\\(%d+)', string.char)
+
+ -- TODO: When libtermkey is patched to accept BEL as an OSC terminator, this workaround can
+ -- be removed
+ seq = seq:gsub('\007$', '\027\\')
+
+ cb(cap, seq)
+
+ count = count - 1
+ if count == 0 then
+ return true
+ end
+ end
+ end,
+ })
+
+ local encoded = {} ---@type string[]
+ for i = 1, #caps do
+ encoded[i] = vim.text.hexencode(caps[i])
+ end
+
+ local query = string.format('\027P+q%s\027\\', table.concat(encoded, ';'))
+ io.stdout:write(query)
+end
+
+return M
diff --git a/runtime/lua/vim/text.lua b/runtime/lua/vim/text.lua
new file mode 100644
index 0000000000..cfb0f9b821
--- /dev/null
+++ b/runtime/lua/vim/text.lua
@@ -0,0 +1,32 @@
+--- Text processing functions.
+
+local M = {}
+
+--- Hex encode a string.
+---
+--- @param str string String to encode
+--- @return string Hex encoded string
+function M.hexencode(str)
+ local bytes = { str:byte(1, #str) }
+ local enc = {} ---@type string[]
+ for i = 1, #bytes do
+ enc[i] = string.format('%02X', bytes[i])
+ end
+ return table.concat(enc)
+end
+
+--- Hex decode a string.
+---
+--- @param enc string String to decode
+--- @return string Decoded string
+function M.hexdecode(enc)
+ assert(#enc % 2 == 0, 'string must have an even number of hex characters')
+ local str = {} ---@type string[]
+ for i = 1, #enc, 2 do
+ local n = assert(tonumber(enc:sub(i, i + 1), 16))
+ str[#str + 1] = string.char(n)
+ end
+ return table.concat(str)
+end
+
+return M
diff --git a/runtime/lua/vim/ui/clipboard/osc52.lua b/runtime/lua/vim/ui/clipboard/osc52.lua
index 035a6abb86..6483f0387d 100644
--- a/runtime/lua/vim/ui/clipboard/osc52.lua
+++ b/runtime/lua/vim/ui/clipboard/osc52.lua
@@ -1,60 +1,75 @@
local M = {}
-function M.copy(lines)
- local s = table.concat(lines, '\n')
- io.stdout:write(string.format('\027]52;;%s\027\\', vim.base64.encode(s)))
+--- Return the OSC 52 escape sequence
+---
+--- @param clipboard string The clipboard to read from or write to
+--- @param contents string The Base64 encoded contents to write to the clipboard, or '?' to read
+--- from the clipboard
+local function osc52(clipboard, contents)
+ return string.format('\027]52;%s;%s\027\\', clipboard, contents)
end
-function M.paste()
- local contents = nil
- local id = vim.api.nvim_create_autocmd('TermResponse', {
- callback = function(args)
- local resp = args.data ---@type string
- local encoded = resp:match('\027%]52;%w?;([A-Za-z0-9+/=]*)')
- if encoded then
- contents = vim.base64.decode(encoded)
- return true
- end
- end,
- })
-
- io.stdout:write('\027]52;;?\027\\')
-
- local ok, res
-
- -- Wait 1s first for terminals that respond quickly
- ok, res = vim.wait(1000, function()
- return contents ~= nil
- end)
-
- if res == -1 then
- -- If no response was received after 1s, print a message and keep waiting
- vim.api.nvim_echo(
- { { 'Waiting for OSC 52 response from the terminal. Press Ctrl-C to interrupt...' } },
- false,
- {}
- )
- ok, res = vim.wait(9000, function()
+function M.copy(reg)
+ local clipboard = reg == '+' and 'c' or 'p'
+ return function(lines)
+ local s = table.concat(lines, '\n')
+ io.stdout:write(osc52(clipboard, vim.base64.encode(s)))
+ end
+end
+
+function M.paste(reg)
+ local clipboard = reg == '+' and 'c' or 'p'
+ return function()
+ local contents = nil
+ local id = vim.api.nvim_create_autocmd('TermResponse', {
+ callback = function(args)
+ local resp = args.data ---@type string
+ local encoded = resp:match('\027%]52;%w?;([A-Za-z0-9+/=]*)')
+ if encoded then
+ contents = vim.base64.decode(encoded)
+ return true
+ end
+ end,
+ })
+
+ io.stdout:write(osc52(clipboard, '?'))
+
+ local ok, res
+
+ -- Wait 1s first for terminals that respond quickly
+ ok, res = vim.wait(1000, function()
return contents ~= nil
end)
- end
- if not ok then
- vim.api.nvim_del_autocmd(id)
if res == -1 then
- vim.notify(
- 'Timed out waiting for a clipboard response from the terminal',
- vim.log.levels.WARN
+ -- If no response was received after 1s, print a message and keep waiting
+ vim.api.nvim_echo(
+ { { 'Waiting for OSC 52 response from the terminal. Press Ctrl-C to interrupt...' } },
+ false,
+ {}
)
- elseif res == -2 then
- -- Clear message area
- vim.api.nvim_echo({ { '' } }, false, {})
+ ok, res = vim.wait(9000, function()
+ return contents ~= nil
+ end)
+ end
+
+ if not ok then
+ vim.api.nvim_del_autocmd(id)
+ if res == -1 then
+ vim.notify(
+ 'Timed out waiting for a clipboard response from the terminal',
+ vim.log.levels.WARN
+ )
+ elseif res == -2 then
+ -- Clear message area
+ vim.api.nvim_echo({ { '' } }, false, {})
+ end
+ return 0
end
- return 0
- end
- -- If we get here, contents should be non-nil
- return vim.split(assert(contents), '\n')
+ -- If we get here, contents should be non-nil
+ return vim.split(assert(contents), '\n')
+ end
end
return M
diff --git a/runtime/plugin/osc52.lua b/runtime/plugin/osc52.lua
new file mode 100644
index 0000000000..374b70066f
--- /dev/null
+++ b/runtime/plugin/osc52.lua
@@ -0,0 +1,36 @@
+local tty = vim.iter(vim.api.nvim_list_uis()):any(function(ui)
+ return ui.chan == 1 and ui.stdout_tty
+end)
+
+if not tty or vim.g.clipboard ~= nil or vim.o.clipboard ~= '' or not os.getenv('SSH_TTY') then
+ return
+end
+
+require('vim.termcap').query('Ms', function(cap, seq)
+ assert(cap == 'Ms')
+
+ -- Check 'clipboard' and g:clipboard again to avoid a race condition
+ if vim.o.clipboard ~= '' or vim.g.clipboard ~= nil then
+ return
+ end
+
+ -- If the terminal reports a sequence other than OSC 52 for the Ms capability
+ -- then ignore it. We only support OSC 52 (for now)
+ if not seq:match('^\027%]52') then
+ return
+ end
+
+ local osc52 = require('vim.ui.clipboard.osc52')
+
+ vim.g.clipboard = {
+ name = 'OSC 52',
+ copy = {
+ ['+'] = osc52.copy('+'),
+ ['*'] = osc52.copy('*'),
+ },
+ paste = {
+ ['+'] = osc52.paste('+'),
+ ['*'] = osc52.paste('*'),
+ },
+ }
+end)