-
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: rewrite to use mappings (for most modes) (#59)
* refactor: rewrite to use mappings * fix: formatting * refactor: properly detect when a sequence shouldn't continue * cleanup * softer deprecation * update readme for rewrite * fix: remove nowait to allow remapping keys * feat: allow disabling mappings and update readme * refactor: swap expr-mappings with `feedkeys` to allow modifying the buffer in functions * docs: update readme * docs: update readme --------- Co-authored-by: Max <[email protected]>
- Loading branch information
1 parent
7e86eda
commit baa6048
Showing
2 changed files
with
244 additions
and
192 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = "<Esc>", | ||
mappings = { | ||
i = { | ||
j = { | ||
k = "<Esc>", | ||
j = "<Esc>", | ||
}, | ||
}, | ||
c = { | ||
j = { | ||
k = "<Esc>", | ||
j = "<Esc>", | ||
}, | ||
}, | ||
t = { | ||
j = { | ||
k = "<Esc>", | ||
j = "<Esc>", | ||
}, | ||
}, | ||
v = { | ||
j = { | ||
k = "<Esc>", | ||
}, | ||
}, | ||
s = { | ||
j = { | ||
k = "<Esc>", | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
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("<BS><BS>" .. 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 <backspace> when escaping | ||
local undo_key = { | ||
i = "<bs>", | ||
c = "<bs>", | ||
t = "<bs>", | ||
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("<cmd>setlocal %smodified<cr>"):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 |
Oops, something went wrong.