aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua')
-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
8 files changed, 217 insertions, 79 deletions
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