Skip to content

Commit

Permalink
Don't get selection via buf_get_text, just cut and restore registers.
Browse files Browse the repository at this point in the history
Easier than taking care of all the stupid edge-cases, like
multibyte-combined-characters or virtualedit.
  • Loading branch information
L3MON4D3 committed Nov 10, 2023
1 parent a9f6925 commit 85e0d03
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 153 deletions.
4 changes: 3 additions & 1 deletion DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -3455,7 +3455,9 @@ These are the settings you can provide to `luasnip.setup()`:
fine (alternatively, this can also be mapped using
`<Plug>luasnip-delete-check`).
- `store_selection_keys`: Mapping for populating `TM_SELECTED_TEXT` and related
variables (not set by default).
variables (not set by default).
If you want to set this mapping yourself, map `ls.select_keys` (not a
function, actually a string/key-combination) as a rhs.
- `enable_autosnippets`: Autosnippets are disabled by default to minimize
performance penalty if unused. Set to `true` to enable.
- `ext_opts`: Additional options passed to extmarks. Can be used to add
Expand Down
5 changes: 3 additions & 2 deletions lua/luasnip/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,9 @@ c = {
if session.config.store_selection_keys then
vim.cmd(
string.format(
[[xnoremap <silent> %s :lua require('luasnip.util.util').store_selection()<cr>gv"_s]],
session.config.store_selection_keys
[[xnoremap <silent> %s %s]],
session.config.store_selection_keys,
require("luasnip.util.select").select_keys
)
)
end
Expand Down
1 change: 1 addition & 0 deletions lua/luasnip/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@ local ls_lazy = {
config = function() return require("luasnip.config") end,
multi_snippet = function() return require("luasnip.nodes.multiSnippet").new_multisnippet end,
snippet_source = function() return require("luasnip.session.snippet_collection.source") end,
select_keys = function() return require("luasnip.util.select").select_keys end
}

ls = util.lazy_table({
Expand Down
3 changes: 2 additions & 1 deletion lua/luasnip/util/_builtin_vars.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local util = require("luasnip.util.util")
local select_util = require("luasnip.util.select")
local lazy_vars = {}

-- Variables defined in https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables
Expand Down Expand Up @@ -156,7 +157,7 @@ local function eager_vars(info)
vars.TM_LINE_INDEX = tostring(pos[1])
vars.TM_LINE_NUMBER = tostring(pos[1] + 1)
vars.LS_SELECT_RAW, vars.LS_SELECT_DEDENT, vars.TM_SELECTED_TEXT =
util.get_selection()
select_util.retrieve()
-- These are for backward compatibility, for now on all builtins that are not part of TM_ go in LS_
vars.SELECT_RAW, vars.SELECT_DEDENT =
vars.LS_SELECT_RAW, vars.LS_SELECT_DEDENT
Expand Down
152 changes: 152 additions & 0 deletions lua/luasnip/util/select.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
local M = {}

local SELECT_RAW = "LUASNIP_SELECT_RAW"
local SELECT_DEDENT = "LUASNIP_SELECT_DEDENT"
local TM_SELECT = "LUASNIP_TM_SELECT"

function M.retrieve()
local ok, val = pcall(vim.api.nvim_buf_get_var, 0, SELECT_RAW)
if ok then
local result = {
val,
vim.api.nvim_buf_get_var(0, SELECT_DEDENT),
vim.api.nvim_buf_get_var(0, TM_SELECT),
}

vim.api.nvim_buf_del_var(0, SELECT_RAW)
vim.api.nvim_buf_del_var(0, SELECT_DEDENT)
vim.api.nvim_buf_del_var(0, TM_SELECT)

return unpack(result)
end
return {}, {}, {}
end

local function get_min_indent(lines)
-- "^(%s*)%S": match only lines that actually contain text.
local min_indent = lines[1]:match("^(%s*)%S")
for i = 2, #lines do
-- %s* -> at least matches
local line_indent = lines[i]:match("^(%s*)%S")
-- ignore if not matched.
if line_indent then
-- if no line until now matched, use line_indent.
if not min_indent or #line_indent < #min_indent then
min_indent = line_indent
end
end
end
return min_indent
end

local function store_registers(...)
local names = { ... }
local restore_data = {}
for _, name in ipairs(names) do
restore_data[name] = {
data = vim.fn.getreg(name),
type = vim.fn.getregtype(name),
}
end
return restore_data
end

local function restore_registers(restore_data)
for name, name_restore_data in pairs(restore_data) do
vim.fn.setreg(name, name_restore_data.data, name_restore_data.type)
end
end

-- subtle: `:lua` exits VISUAL, which means that the '< '>-marks will be set correctly!
-- Afterwards, we can just use <cmd>lua, which does not change the mode.
M.select_keys =
[[:lua require("luasnip.util.select").pre_cut()<Cr>gv"zs<cmd>lua require('luasnip.util.select').post_cut("z")<Cr>]]

local saved_registers
local lines
local start_line, start_col, end_line, end_col
local mode
function M.pre_cut()
-- store registers so we don't change any of them.
-- "" is affected since we perform a cut (s), 1-9 also (although :h
-- quote_number seems to state otherwise for cuts to specific registers..?).
saved_registers = store_registers(
"",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"z"
)

-- store data needed for de-indenting lines.
start_line = vim.fn.line("'<") - 1
start_col = vim.fn.col("'<")
end_line = vim.fn.line("'>") - 1
end_col = vim.fn.col("'>")
-- +1: include final line.
lines = vim.api.nvim_buf_get_lines(0, start_line, end_line + 1, true)
mode = vim.fn.visualmode()
end

function M.post_cut(register_name)
-- remove trailing newline.
local chunks = vim.split(vim.fn.getreg(register_name):gsub("\n$", ""), "\n")

-- make sure to restore the registers to the state they were before cutting.
restore_registers(saved_registers)

local tm_select, select_dedent = vim.deepcopy(chunks), vim.deepcopy(chunks)

local min_indent = get_min_indent(lines) or ""
if mode == "V" then
tm_select[1] = tm_select[1]:gsub("^%s+", "")
-- remove indent from all lines:
for i = 1, #select_dedent do
select_dedent[i] = select_dedent[i]:gsub("^" .. min_indent, "")
end
-- due to the trailing newline of the last line, and vim.split's
-- behaviour, the last line of `chunks` is always empty.
-- Keep this
elseif mode == "v" then
-- if selection starts inside indent, remove indent.
if #min_indent > start_col then
select_dedent[1] = lines[1]:gsub(min_indent, "")
end
for i = 2, #select_dedent - 1 do
select_dedent[i] = select_dedent[i]:gsub(min_indent, "")
end

-- remove as much indent from the last line as possible.
if #min_indent > end_col then
select_dedent[#select_dedent] = ""
else
select_dedent[#select_dedent] =
select_dedent[#select_dedent]:gsub("^" .. min_indent, "")
end
else
-- in block: if indent is in block, remove the part of it that is inside
-- it for select_dedent.
if #min_indent > start_col then
local indent_in_block = min_indent:sub(start_col, #min_indent)
for i, line in ipairs(chunks) do
select_dedent[i] = line:gsub("^" .. indent_in_block, "")
end
end
end

vim.api.nvim_buf_set_var(0, SELECT_RAW, chunks)
vim.api.nvim_buf_set_var(0, SELECT_DEDENT, select_dedent)
vim.api.nvim_buf_set_var(0, TM_SELECT, tm_select)

lines = nil
start_line, start_col, end_line, end_col = nil, nil, nil, nil
mode = nil
end

return M
148 changes: 0 additions & 148 deletions lua/luasnip/util/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -268,152 +268,6 @@ local function wrap_nodes(nodes)
end
end

local SELECT_RAW = "LUASNIP_SELECT_RAW"
local SELECT_DEDENT = "LUASNIP_SELECT_DEDENT"
local TM_SELECT = "LUASNIP_TM_SELECT"

local function get_selection()
local ok, val = pcall(vim.api.nvim_buf_get_var, 0, SELECT_RAW)
if ok then
local result = {
val,
vim.api.nvim_buf_get_var(0, SELECT_DEDENT),
vim.api.nvim_buf_get_var(0, TM_SELECT),
}

vim.api.nvim_buf_del_var(0, SELECT_RAW)
vim.api.nvim_buf_del_var(0, SELECT_DEDENT)
vim.api.nvim_buf_del_var(0, TM_SELECT)

return unpack(result)
end
return {}, {}, {}
end

local function get_min_indent(lines)
-- "^(%s*)%S": match only lines that actually contain text.
local min_indent = lines[1]:match("^(%s*)%S")
for i = 2, #lines do
-- %s* -> at least matches
local line_indent = lines[i]:match("^(%s*)%S")
-- ignore if not matched.
if line_indent then
-- if no line until now matched, use line_indent.
if not min_indent or #line_indent < #min_indent then
min_indent = line_indent
end
end
end
return min_indent
end

-- nvim0.7 is missing vim.v.maxcol :/
-- but this should suffice.
local maxcol = vim.v.maxcol or 2147483647
-- displaycol_to may be nil for end-of-line
local function displaycol_trim_line(line, displaycol_from, displaycol_to)
local line_len = vim.str_utfindex(line, #line)

-- bytes are 0-based, start- and end-inclusive.
-- str_byteindex gives last byte, get first byte is last byte of previous
-- symbol +1.
local start_byte = vim.str_byteindex(line, displaycol_from - 1) + 1
local end_byte =
vim.str_byteindex(line, math.min(displaycol_to or maxcol, line_len))

return line:sub(start_byte, end_byte)
end

local function store_selection()
-- _col are positions in display-columns, so utf-index, 1-based.
-- lines are 0-based.
local _, start_line, start_col, _ = unpack(vim.fn.getcharpos("'<"))
start_line = start_line - 1

local _, end_line, end_col, _ = unpack(vim.fn.getcharpos("'>"))
end_line = end_line - 1

local mode = vim.fn.visualmode()
if
not vim.o.selection == "exclusive"
and not (start_line == end_line and start_col == end_col)
then
end_col = end_col - 1
end

-- include final line.
local lines = vim.api.nvim_buf_get_lines(0, start_line, end_line + 1, true)

local chunks = {}
if start_line == end_line then
chunks = { displaycol_trim_line(lines[1], start_col, end_col) }
else
-- displaycolumns!
local first_col = 1
local last_col = nil
if mode:lower() ~= "v" then -- mode is block
first_col = start_col
last_col = end_col
end
chunks = { displaycol_trim_line(lines[1], start_col, last_col) }

-- potentially trim lines (Block).
for cl = 2, #lines - 1 do
table.insert(
chunks,
displaycol_trim_line(lines[cl], first_col, last_col)
)
end
table.insert(
chunks,
displaycol_trim_line(lines[#lines], first_col, end_col)
)
end

-- init with raw selection.
local tm_select, select_dedent = vim.deepcopy(chunks), vim.deepcopy(chunks)
-- may be nil if no indent.
local min_indent = get_min_indent(lines) or ""
-- TM_SELECTED_TEXT contains text from new cursor position(for V the first
-- non-whitespace of first line, v and c-v raw) to end of selection.
if mode == "V" then
tm_select[1] = tm_select[1]:gsub("^%s+", "")
-- remove indent from all lines:
for i = 1, #select_dedent do
select_dedent[i] = select_dedent[i]:gsub("^" .. min_indent, "")
end
elseif mode == "v" then
-- if selection starts inside indent, remove indent.
if #min_indent > start_col then
select_dedent[1] = lines[1]:gsub(min_indent, "")
end
for i = 2, #select_dedent - 1 do
select_dedent[i] = select_dedent[i]:gsub(min_indent, "")
end

-- remove as much indent from the last line as possible.
if #min_indent > end_col then
select_dedent[#select_dedent] = ""
else
select_dedent[#select_dedent] =
select_dedent[#select_dedent]:gsub("^" .. min_indent, "")
end
else
-- in block: if indent is in block, remove the part of it that is inside
-- it for select_dedent.
if #min_indent > start_col then
local indent_in_block = min_indent:sub(start_col, #min_indent)
for i, line in ipairs(chunks) do
select_dedent[i] = line:gsub("^" .. indent_in_block, "")
end
end
end

vim.api.nvim_buf_set_var(0, SELECT_RAW, chunks)
vim.api.nvim_buf_set_var(0, SELECT_DEDENT, select_dedent)
vim.api.nvim_buf_set_var(0, TM_SELECT, tm_select)
end

local function pos_equal(p1, p2)
return p1[1] == p2[1] and p1[2] == p2[2]
end
Expand Down Expand Up @@ -646,8 +500,6 @@ return {
put = put,
to_string_table = to_string_table,
wrap_nodes = wrap_nodes,
store_selection = store_selection,
get_selection = get_selection,
pos_equal = pos_equal,
dedent = dedent,
indent = indent,
Expand Down
4 changes: 3 additions & 1 deletion tests/integration/parser_spec.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
local helpers = require("test.functional.helpers")(after_each)
local exec_lua, feed = helpers.exec_lua, helpers.feed
local exec_lua, feed, exec = helpers.exec_lua, helpers.feed, helpers.exec
local ls_helpers = require("helpers")
local Screen = require("test.functional.ui.screen")

Expand Down Expand Up @@ -725,6 +725,8 @@ describe("Parser", function()

-- expand snippet with selected multiline-text.
feed("iasdf<Cr>asdf<Esc>Vk<Tab>")
-- wait a bit..
exec('call wait(200, "0")')
exec_lua("ls.lsp_expand([[" .. snip .. "]])")

screen:expect({
Expand Down
Loading

0 comments on commit 85e0d03

Please sign in to comment.