diff --git a/lua/better_escape.lua b/lua/better_escape.lua index fe576ea..e97e5f7 100644 --- a/lua/better_escape.lua +++ b/lua/better_escape.lua @@ -1,175 +1,167 @@ local M = {} +local uv = vim.uv +local t = vim.keycode -local api = vim.api +M.waiting = false local settings = { timeout = vim.o.timeoutlen, - mapping = { "jk", "jj" }, - clear_empty_lines = false, - ---@type string|function - keys = "", + mappings = { + i = { + j = { + k = "", + j = "", + }, + }, + c = { + j = { + k = "", + j = "", + }, + }, + t = { + j = { + k = "", + j = "", + }, + }, + v = { + j = { + k = "", + }, + }, + s = { + j = { + k = "", + }, + }, + }, } -local first_chars = {} -local second_chars = {} - ----@class State ----@field char string ----@field modified boolean - -local timer -local waiting = false ----@type State[] -local input_states = {} - ----@param tbl table table to search through ----@param element any element to search in tbl ----@return table indices ---- Search for indices in tbl where element occurs -local function get_indices(tbl, element) - local indices = {} - for idx, value in ipairs(tbl) do - if element == value then - table.insert(indices, idx) +local function clear_mappings() + for mode, keys in pairs(settings.mappings) do + for key, subkeys in pairs(keys) do + vim.keymap.del(mode, key) + for subkey, _ in pairs(subkeys) do + vim.keymap.del(mode, subkey) + end end end - return indices end ----@param keys string keys to feed ---- Replace keys with termcodes and feed them -local function feed(keys, mode) - api.nvim_feedkeys( - api.nvim_replace_termcodes(keys, true, true, true), - mode or "n", - false - ) +-- WIP: move this into recorder.lua ? +local last_key = nil +local bufmodified = false +local sequence_timer = uv.new_timer() +local recorded_key = false +local function log_key(key) + bufmodified = vim.bo.modified + last_key = key + recorded_key = true + sequence_timer:stop() + M.waiting = true + sequence_timer:start(settings.timeout, 0, function() + M.waiting = false + if last_key == key then + last_key = nil + end + end) end -local function start_timer() - waiting = true - - if timer then - timer:stop() +vim.on_key(function(mappings, typed) + if typed == "" then + return end - - timer = vim.defer_fn(function() - waiting = false - end, settings.timeout) -end - -local function get_keys() - -- if keys is string use it, else use it as a function - return type(settings.keys) == "string" and settings.keys or settings.keys() -end - -local function check_timeout() - if waiting then - local current_line = api.nvim_get_current_line() - if settings.clear_empty_lines and current_line:match("^%s+j$") then - vim.schedule(function() - api.nvim_set_current_line("") - feed(get_keys(), "in") - end) - else - feed("" .. get_keys(), "in") -- delete the characters from the mapping - end - - waiting = false -- more timely - return true + if recorded_key then + recorded_key = false + return end - return false -end - -function M.check_charaters() - local char = vim.v.char - table.insert(input_states, { char = char, modified = vim.bo.modified }) - - local matched = false - if #input_states >= 2 then - ---@type State - local prev_state = input_states[#input_states - 1] - local indices = get_indices(second_chars, char) - -- if char == second_chars[idx] and prev_char == first_chars[idx] as well - -- then matched = true - for _, idx in ipairs(indices) do - if first_chars[idx] == prev_state.char then - matched = check_timeout() - break - end - end - - if matched then - input_states = {} - vim.schedule(function() - vim.bo.modified = prev_state.modified + last_key = nil +end) + +-- list of modes that press when escaping +local undo_key = { + i = "", + c = "", + t = "", + v = "", + s = "", +} +local parent_keys = {} + +local function map_keys() + parent_keys = {} + for mode, keys in pairs(settings.mappings) do + for key, subkeys in pairs(keys) do + vim.keymap.set(mode, key, function() + log_key(key) + vim.api.nvim_feedkeys(t(key), "in", false) end) + for subkey, mapping in pairs(subkeys) do + if mapping then + if not parent_keys[mode] then + parent_keys[mode] = {} + end + if not parent_keys[mode][subkey] then + parent_keys[mode][subkey] = {} + end + parent_keys[mode][subkey][key] = true + vim.keymap.set(mode, subkey, function() + -- In case the subkey happens to also be a starting key + if last_key == nil then + log_key(subkey) + vim.api.nvim_feedkeys(t(subkey), "in", false) + return + end + -- Make sure we are in the correct sequence + if not parent_keys[mode][subkey][last_key] then + vim.api.nvim_feedkeys(t(subkey), "in", false) + return + end + vim.api.nvim_feedkeys(t(undo_key[mode] or ""), "in", false) + vim.api.nvim_feedkeys( + t("setlocal %smodified"):format( + bufmodified and "" or "no" + ) + , "in", false) + if type(mapping) == "string" then + vim.api.nvim_input(mapping) + elseif type(mapping) == "function" then + mapping() + end + end) + end + end end end - - -- if can't find a match, and the typed char is first in a mapping, start the timeout - if not matched and vim.tbl_contains(first_chars, char) then - start_timer() - end -end - -local function char_at(str, pos) - return vim.fn.nr2char(vim.fn.strgetchar(str, pos)) -end - -local function validate_settings() - assert(type(settings.mapping) == "table", "Mapping must be a table.") - - for _, mapping in ipairs(settings.mapping) do - -- replace all multibyte chars to `A` char - local length = #vim.fn.substitute(mapping, ".", "A", "g") - assert(length == 2, "Mapping must be 2 keys.") - end - - if settings.timeout then - assert(type(settings.timeout) == "number", "Timeout must be a number.") - assert( - settings.timeout >= 100, - "Timeout must be greater than or equal to 100." - ) - end - - assert( - vim.tbl_contains({ "string", "function" }, type(settings.keys)), - "Keys must be a function or string." - ) end function M.setup(update) settings = vim.tbl_deep_extend("force", settings, update or {}) - -- if mapping is a string (single mapping) make it a table - if type(settings.mapping) == "string" then - settings.mapping = { settings.mapping } + if settings.keys or settings.clear_empty_lines then + vim.notify( + "[better-escape.nvim]: Rewrite! Check: https://github.com/max397574/better-escape.nvim", + vim.log.levels.WARN, + {} + ) end - local ok, msg = pcall(validate_settings) - if ok then - -- create tables with the first and seconds chars of the mappings - for _, shortcut in ipairs(settings.mapping) do - vim.cmd("silent! iunmap " .. shortcut) - table.insert(first_chars, char_at(shortcut, 0)) - table.insert(second_chars, char_at(shortcut, 1)) + if settings.mapping then + vim.notify( + "[better-escape.nvim]: Rewrite! Check: https://github.com/max397574/better-escape.nvim", + vim.log.levels.WARN, + {} + ) + if type(settings.mapping) == "string" then + settings.mapping = { settings.mapping } + end + for _, mapping in ipairs(settings.mappings) do + settings.mappings.i[mapping:sub(1, 2)] = {} + settings.mappings.i[mapping:sub(1, 1)][mapping:sub(2, 2)] = + settings.keys end - - vim.cmd([[ - augroup better_escape - autocmd! - autocmd InsertCharPre * lua require"better_escape".check_charaters() - augroup END - ]]) - else - vim.notify("Error(better-escape.nvim): " .. msg, vim.log.levels.ERROR) end + pcall(clear_mappings) + map_keys() end -return setmetatable(M, { - __index = function(_, k) - if k == "waiting" then - return waiting - end - end, -}) +return M diff --git a/readme.md b/readme.md index e856256..2a603b8 100644 --- a/readme.md +++ b/readme.md @@ -1,21 +1,24 @@ -# 🚪better-escape.nvim +# better-escape.nvim -This plugin is the lua version of [better_escape.vim](https://github.com/jdhao/better-escape.vim), -with some additional features and optimizations +![better-escape](https://github.com/max397574/better-escape.nvim/assets/81827001/8863a620-b075-4417-92d0-7eb2d2646186) -A lot of people have mappings like `jk` or `jj` to escape insert mode. -The problem with this mappings is that whenever you type a `j`, neovim wait about 100-500ms (depending on your timeoutlen) to see, if you type a `j` or a `k` because these are mapped. -Only after that time the `j` will be inserted. -Then you always get a delay when typing a `j`. +A lot of people have mappings like `jk` or `jj` to escape insert mode. The +problem with this mappings is that whenever you type a `j`, neovim wait about +100-500ms (depending on your timeoutlen) to see, if you type a `j` or a `k` +because these are mapped. Only after that time the `j` will be inserted. Then +you always get a delay when typing a `j`. -This looks like this (see below for a gif): +An example where this has a big impact is e.g. telescope. Because the characters +which are mapped aren't really inserted at first the whole filtering isn't +instant. -![Screen Shot 2021-10-08 at 16 21 23](https://user-images.githubusercontent.com/81827001/136576543-c8b4e802-84a8-4087-a7a4-f7d069931885.png) +![better-escape-tele](https://github.com/max397574/better-escape.nvim/assets/81827001/390f115d-87cd-43d8-aadf-fffb12bd84c9) ## ✨Features -- Escape without getting delay when typing in insert mode -- Customizable mapping and timeout +- Write mappings in many modes without having a delay when typing +- Customizable timeout +- Map key sequences and lua functions - Use multiple mappings - Really small and fast @@ -24,8 +27,8 @@ This looks like this (see below for a gif): Use your favourite package manager and call the setup function. ```lua --- lua with packer.nvim -use { +-- lua with lazy.nvim +{ "max397574/better-escape.nvim", config = function() require("better_escape").setup() @@ -33,31 +36,101 @@ use { } ``` +## ❗Rewrite + +There was a big rewrite which allows much more flexibility now. You can now +define mappings in most modes and also use functions. + +The biggest change was that the `mapping` config option was removed. Check the +default configuration below to see the new structure. + +This also deprecated the `clear_empty_lines` setting. You can replicate this +behavior with a function like this: + +```lua +k = function() + vim.api.nvim_input("") + local current_line = vim.api.nvim_get_current_line() + if current_line:match("^%s+j$") then + vim.api.nvim_set_current_line("") + end +end +``` + ## ⚙️Customization Call the setup function with your options as arguments. +After the rewrite you can also use any function. So you could for example map +`` to jump with luasnip like this: + +```lua +i = { + [" "] = { + [""] = function() + -- Defer execution to avoid side-effects + vim.defer_fn(function() + -- set undo point + vim.o.ul = vim.o.ul + require("luasnip").expand_or_jump() + end, 1) + end + } +} +``` + +To disable keys set them to `false` in the configuration. + +
+Default Config + ```lua -- lua, default settings require("better_escape").setup { - mapping = {"jk", "jj"}, -- a table with mappings to use - timeout = vim.o.timeoutlen, -- the time in which the keys must be hit in ms. Use option timeoutlen by default - clear_empty_lines = false, -- clear line after escaping if there is only whitespace - keys = "", -- keys used for escaping, if it is a function will use the result everytime - -- example(recommended) - -- keys = function() - -- return vim.api.nvim_win_get_cursor(0)[2] > 1 and 'l' or '' - -- end, + timeout = vim.o.timeoutlen, + mappings = { + i = { + j = { + -- These can all also be functions + k = "", + j = "", + }, + }, + c = { + j = { + k = "", + j = "", + }, + }, + t = { + j = { + k = "", + j = "", + }, + }, + v = { + j = { + k = "", + }, + }, + s = { + j = { + k = "", + }, + }, + }, } ``` +
+ ## API `require("better_escape").waiting` is a boolean indicating that it's waiting for a mapped sequence to complete.
-statusline example +Statusline example ```lua function escape_status() @@ -68,28 +141,15 @@ end
-## 👀Demo - -![mapping](https://user-images.githubusercontent.com/81827001/135870002-07c1dc41-f3e7-4ece-af6f-50e9b0711a66.gif) - -![plugin](https://user-images.githubusercontent.com/81827001/135870101-febf3507-9327-4b80-aa9a-ba08bff6b8d4.gif) - -## 🎓How it works - -With the mappings there are two tables created. -One contains all first characters and one all second characters. -Whenever you type a character the plugin checks if it's in any of the two tables -If it is in the first one, the plugin starts a timer. -If it is in the second, the plugin checks whether the character you typed before is in the table with the first characters. +## ❤️ Support -If this is the case the plugin gets all the indices where the characters are in the tables, then is searches for matches. -If there is a match, that means that there is a mapping which has the typed character as second and the previous typed character as first character. -The plugin then checks if the time passed since the first character was types is smaller than `timoutlen`. -If this is the case the two characters get deleted and `keys` get feed or executed. +If you like the projects I do and they can help you in your life you can support +my work with [github sponsors](https://github.com/sponsors/max397574). Every +support motivates me to continue working on my open source projects. -Like this it is possible that the characters really get inserted and therefore you have no delay after typing one of the characters of your mapping. -With the `timeoutlen` it's still possible to type the characters of your mappings. +## Similar plugins -## ❤️ Support -If you like the projects I do and they can help you in your life you can support my work with [github sponsors](https://github.com/sponsors/max397574). -Every support motivates me to continue working on my open source projects. +The old version of this plugin was a lua version of +[better_escape.vim](https://github.com/jdhao/better-escape.vim), with some +additional features and optimizations. This changed with the rewrite though. Now +it has much more features.