diff options
Diffstat (limited to 'runtime/lua')
-rw-r--r-- | runtime/lua/vim/_defaults.lua | 15 | ||||
-rw-r--r-- | runtime/lua/vim/_init_packages.lua | 1 | ||||
-rw-r--r-- | runtime/lua/vim/_meta/api.lua | 7 | ||||
-rw-r--r-- | runtime/lua/vim/_meta/options.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/snippet.lua | 72 | ||||
-rw-r--r-- | runtime/lua/vim/termcap.lua | 60 | ||||
-rw-r--r-- | runtime/lua/vim/text.lua | 32 | ||||
-rw-r--r-- | runtime/lua/vim/ui/clipboard/osc52.lua | 107 |
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 |