Skip to content

Commit

Permalink
feat: ensure strict ordering of feedkeys initiated by luasnip.
Browse files Browse the repository at this point in the history
This allows us to first of all, be certain that successive ls.jump()
always end up at the correct position, but also enables running all
these feedkeys with "ni", which means that they will be executed at the
correct time even if the typeahead contains other keys! This makes
luasnip behave deterministically in macros, which was certainly not the
case before!

We can also finally remove all the ugly vim.wait(...)-calls from the
tests :D (at least those related to waiting on luasnip-actions to
complete, we still need some related to filesystem-events.)
Now, these only have to happen for actions that are not hooked into the
feedkeys-action-queue, like `ls.expand`. IMO while not perfect, this is
a great improvement because `ls.expand` are rarely called back-to-back,
while `ls.jump` are.

A last neat addition is that autosnippets can be expanded properly when
replaying a macro by inserting expand_auto before other typeahead, thus
making it take place at the correct position.
  • Loading branch information
L3MON4D3 committed Oct 28, 2024
1 parent 787dee5 commit 8566299
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 165 deletions.
8 changes: 1 addition & 7 deletions lua/luasnip/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,7 @@ c = {
end)
if session.config.enable_autosnippets then
ls_autocmd("InsertCharPre", function()
Luasnip_just_inserted = true
end)
ls_autocmd({ "TextChangedI", "TextChangedP" }, function()
if Luasnip_just_inserted then
require("luasnip").expand_auto()
Luasnip_just_inserted = nil
end
require("luasnip.util.feedkeys").feedkeys_insert("<cmd>lua require('luasnip').expand_auto()<cr>")
end)
end

Expand Down
27 changes: 4 additions & 23 deletions lua/luasnip/nodes/insertNode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ local node_util = require("luasnip.nodes.util")
local types = require("luasnip.util.types")
local events = require("luasnip.util.events")
local extend_decorator = require("luasnip.util.extend_decorator")
local feedkeys = require("luasnip.util.feedkeys")

local function I(pos, static_text, opts)
static_text = util.to_string_table(static_text)
Expand Down Expand Up @@ -49,16 +50,7 @@ function ExitNode:input_enter(no_move, dry_run)
self.parent:subtree_set_pos_rgrav(begin_pos, 1, true)

if not no_move then
if vim.fn.mode() == "i" then
util.insert_move_on(begin_pos)
else
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Esc>", true, false, true),
"n",
true
)
util.normal_move_on_insert(begin_pos)
end
feedkeys.insert_at(begin_pos)
end

self:event(events.enter)
Expand Down Expand Up @@ -122,20 +114,9 @@ function InsertNode:input_enter(no_move, dry_run)
-- SELECT snippet text only when there is text to select (more oft than not there isnt).
local mark_begin_pos, mark_end_pos = self.mark:pos_begin_end_raw()
if not util.pos_equal(mark_begin_pos, mark_end_pos) then
util.any_select(mark_begin_pos, mark_end_pos)
feedkeys.select_range(mark_begin_pos, mark_end_pos)
else
-- if current and target mode is INSERT, there's no reason to leave it.
if vim.fn.mode() == "i" then
util.insert_move_on(mark_begin_pos)
else
-- mode might be VISUAL or something else, but <Esc> always leads to normal.
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Esc>", true, false, true),
"n",
true
)
util.normal_move_on_insert(mark_begin_pos)
end
feedkeys.insert_at(mark_begin_pos)
end
end

Expand Down
12 changes: 2 additions & 10 deletions lua/luasnip/nodes/textNode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local util = require("luasnip.util.util")
local types = require("luasnip.util.types")
local events = require("luasnip.util.events")
local extend_decorator = require("luasnip.util.extend_decorator")
local feedkeys = require("luasnip.util.feedkeys")

local TextNode = node_mod.Node:new()

Expand All @@ -25,16 +26,7 @@ function TextNode:input_enter(no_move, dry_run)

if not no_move then
local mark_begin_pos = self.mark:pos_begin_raw()
if vim.fn.mode() == "i" then
util.insert_move_on(mark_begin_pos)
else
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Esc>", true, false, true),
"n",
true
)
util.normal_move_on_insert(mark_begin_pos)
end
feedkeys.insert_at(mark_begin_pos)
end

self:event(events.enter, no_move)
Expand Down
3 changes: 2 additions & 1 deletion lua/luasnip/nodes/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local ext_util = require("luasnip.util.ext_opts")
local types = require("luasnip.util.types")
local key_indexer = require("luasnip.nodes.key_indexer")
local session = require("luasnip.session")
local feedkeys = require("luasnip.util.feedkeys")

local function subsnip_init_children(parent, children)
for _, child in ipairs(children) do
Expand Down Expand Up @@ -125,7 +126,7 @@ end

local function select_node(node)
local node_begin, node_end = node.mark:pos_begin_end_raw()
util.any_select(node_begin, node_end)
feedkeys.select_range(node_begin, node_end)
end

local function print_dict(dict)
Expand Down
141 changes: 141 additions & 0 deletions lua/luasnip/util/feedkeys.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
-- insert operations into typeahead buffer in a controlled manner.
-- We want to follow these guidelines:
-- * maintain order of luasnip-operations. Example:
-- `ls.jump() ls.jump()`, the first jump is completely executed, and only
-- then the second jump is.
-- * insert our operations before other typeahead.
-- This is to behave correctly in a macro: IIUC the complete macro is written
-- into typeahead, so if our operations are appended, we will jump way later
-- than in the actual recorded "session", and input won't end up where it
-- belongs.
--
-- We will achieve these goals by only ever having one operation in the
-- typeahead, and once it is finished it calls a callback that will insert any
-- other keys that were requested to be executed.
--
-- This scheme is inspired by @hrsh7th's work in nvim-cmp.
local util = require("luasnip.util.util")

local M = {}

local current_id = 0
local executing_id = nil

-- contains functions which take exactly one argument, the id.
local enqueued_actions = {}

local function _feedkeys_insert(id, keys)
executing_id = id
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes(keys .. "<cmd>lua require('luasnip.util.feedkeys').confirm(" .. id .. ")<cr>", true, false, true),
-- folds are opened manually now, no need to pass t.
-- n prevents langmap from interfering.
"ni",
true
)
end

local function enqueue_action(fn)
-- get unique id and increment global.
local keys_id = current_id
current_id = current_id + 1

-- if there is nothing from luasnip currently executing, we may just insert
-- into the typeahead
if executing_id == nil then
fn(keys_id)
else
enqueued_actions[keys_id] = fn
end
end

function M.feedkeys_insert(keys)
enqueue_action(function(id)
_feedkeys_insert(id, keys)
end)
end

-- pos: (0,0)-indexed.
local function cursor_set_keys(pos, before)
if before then
if pos[2] == 0 then
pos[1] = pos[1] - 1
-- pos2 is set to last columnt of previous line.
-- # counts bytes, but win_set_cursor expects bytes, so all's good.
pos[2] =
#vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)[1]
else
pos[2] = pos[2] - 1
end
end

return "<cmd>lua vim.api.nvim_win_set_cursor(0,{"
-- +1, win_set_cursor starts at 1.
.. pos[1] + 1
.. ","
-- -1 works for multibyte because of rounding, apparently.
.. pos[2]
.. "})"
.. "<cr><cmd>:silent! foldopen!<cr>"
end

function M.select_range(b, e)
enqueue_action(function(id)
-- stylua: ignore
_feedkeys_insert(id,
-- this esc -> movement sometimes leads to a slight flicker
-- TODO: look into preventing that reliably.
-- Go into visual, then place endpoints.
-- This is to allow us to place the cursor on the \n of a line.
-- see #1158
"<esc>"
-- open folds that contain this selection.
-- we assume that the selection is contained in at most one fold, and
-- that that fold covers b.
-- if we open the fold while visual is active, the selection will be
-- wrong, so this is necessary before we enter VISUAL.
.. cursor_set_keys(b)
-- start visual highlight and move to b again.
-- since we are now in visual, this might actually move the cursor.
.. "v"
.. cursor_set_keys(b)
-- swap to other end of selection, and move it to e.
.. "o"
.. (vim.o.selection == "exclusive" and
cursor_set_keys(e) or
-- set before
cursor_set_keys(e, true))
.. "o<C-G><C-r>_" )
end)
end

-- move the cursor to a position and enter insert-mode (or stay in it).
function M.insert_at(pos)
enqueue_action(function(id)
-- if current and target mode is INSERT, there's no reason to leave it.
if vim.fn.mode() == "i" then
-- can skip feedkeys here, we can complete this command from lua.
-- Just have to make sure to call confirm afterward, since there
-- may be more actions enqueued.
-- We don't have to set the executing_id, since there's no way
-- enqueue_action could be called before `confirm`.
util.set_cursor_0ind(pos)
vim.api.nvim_command("redraw!")
M.confirm(id)
else
-- mode might be VISUAL or something else => <Esc> to know we're in NORMAL.
_feedkeys_insert(id, "<Esc>i" .. cursor_set_keys(pos))
end
end)
end

function M.confirm(id)
executing_id = nil

if enqueued_actions[id+1] then
enqueued_actions[id+1](id+1)
enqueued_actions[id+1] = nil
end
end

return M
81 changes: 0 additions & 81 deletions lua/luasnip/util/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -125,84 +125,6 @@ local function bytecol_to_utfcol(pos)
return { pos[1], vim.str_utfindex(line[1] or "", pos[2]) }
end

local function replace_feedkeys(keys, opts)
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes(keys, true, false, true),
-- folds are opened manually now, no need to pass t.
-- n prevents langmap from interfering.
opts or "n",
true
)
end

-- pos: (0,0)-indexed.
local function cursor_set_keys(pos, before)
if before then
if pos[2] == 0 then
pos[1] = pos[1] - 1
-- pos2 is set to last columnt of previous line.
-- # counts bytes, but win_set_cursor expects bytes, so all's good.
pos[2] =
#vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)[1]
else
pos[2] = pos[2] - 1
end
end

return "<cmd>lua vim.api.nvim_win_set_cursor(0,{"
-- +1, win_set_cursor starts at 1.
.. pos[1] + 1
.. ","
-- -1 works for multibyte because of rounding, apparently.
.. pos[2]
.. "})"
.. "<cr><cmd>:silent! foldopen!<cr>"
end

-- any for any mode.
-- other functions prefixed with eg. normal have to be in that mode, the
-- initial esc removes that need.
local function any_select(b, e)
-- stylua: ignore
replace_feedkeys(
-- this esc -> movement sometimes leads to a slight flicker
-- TODO: look into preventing that reliably.
-- Go into visual, then place endpoints.
-- This is to allow us to place the cursor on the \n of a line.
-- see #1158
"<esc>"
-- open folds that contain this selection.
-- we assume that the selection is contained in at most one fold, and
-- that that fold covers b.
-- if we open the fold while visual is active, the selection will be
-- wrong, so this is necessary before we enter VISUAL.
.. cursor_set_keys(b)
-- start visual highlight and move to b again.
-- since we are now in visual, this might actually move the cursor.
.. "v"
.. cursor_set_keys(b)
-- swap to other end of selection, and move it to e.
.. "o"
.. (vim.o.selection == "exclusive" and
cursor_set_keys(e) or
-- set before
cursor_set_keys(e, true))
.. "o<C-G><C-r>_" )
end

local function normal_move_on_insert(new_cur_pos)
-- moving in normal and going into insert is kind of annoying, eg. when the
-- cursor is, in normal, on a tab, i will set it on the beginning of the
-- tab. There's more problems, but this is very safe.
replace_feedkeys("i" .. cursor_set_keys(new_cur_pos))
end

local function insert_move_on(new_cur_pos)
-- maybe feedkeys this too.
set_cursor_0ind(new_cur_pos)
vim.api.nvim_command("redraw!")
end

local function multiline_equal(t1, t2)
for i, line in ipairs(t1) do
if line ~= t2[i] then
Expand Down Expand Up @@ -479,9 +401,6 @@ return {
get_cursor_0ind = get_cursor_0ind,
set_cursor_0ind = set_cursor_0ind,
move_to_mark = move_to_mark,
normal_move_on_insert = normal_move_on_insert,
insert_move_on = insert_move_on,
any_select = any_select,
remove_n_before_cur = remove_n_before_cur,
get_current_line_to_cursor = get_current_line_to_cursor,
line_chars_before = line_chars_before,
Expand Down
4 changes: 0 additions & 4 deletions tests/integration/choice_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,7 @@ describe("ChoiceNode", function()
-- change text in insertNode.
feed("c")
exec_lua("ls.jump(1)")
exec_lua("vim.wait(10, function() end)")
exec_lua("ls.change_choice(1)")
exec_lua("vim.wait(10, function() end)")
screen:expect({
grid = [[
c ^b |
Expand All @@ -118,9 +116,7 @@ describe("ChoiceNode", function()

-- change choice on outer choiceNode.
exec_lua("ls.jump(-1)")
exec_lua("vim.wait(10, function() end)")
exec_lua("ls.change_choice(1)")
exec_lua("vim.wait(10, function() end)")
screen:expect({
grid = [[
^b |
Expand Down
1 change: 0 additions & 1 deletion tests/integration/jump_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ describe("Jumping", function()

-- jump into restoreNode in first choice.
exec_lua("ls.jump(1)")
exec_lua("vim.wait(10, function() end)")
exec_lua("ls.change_choice(1)")
screen:expect({
grid = [[
Expand Down
Loading

0 comments on commit 8566299

Please sign in to comment.