mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	lua: add vim.register_keystroke_callback (#12536)
* feat: Add vim.register_keystroke_callback * fixup: Forgot to remove mention of old option * fixup: Answer jamessan comments * fixup: Answer norcalli comments * fixup: portability * Update runtime/doc/lua.txt Co-authored-by: Ashkan Kiani <ashkan.k.kiani@gmail.com>
This commit is contained in:
		| @@ -928,6 +928,34 @@ vim.region({bufnr}, {pos1}, {pos2}, {type}, {inclusive})       *vim.region()* | |||||||
|         whether the selection is inclusive or not, into a zero-indexed table  |         whether the selection is inclusive or not, into a zero-indexed table  | ||||||
|         of linewise selections of the form `{linenr = {startcol, endcol}}` . |         of linewise selections of the form `{linenr = {startcol, endcol}}` . | ||||||
|  |  | ||||||
|  |                                              *vim.register_keystroke_callback()* | ||||||
|  | vim.register_keystroke_callback({fn}, {ns_id}) | ||||||
|  |         Register a lua {fn} with an {ns_id} to be run after every keystroke. | ||||||
|  |  | ||||||
|  |         Parameters: ~ | ||||||
|  |             {fn}: (function): Function to call on keystroke. | ||||||
|  |                     It should take one argument, which is a string. | ||||||
|  |                     The string will contain the literal keys typed. | ||||||
|  |                     See |i_CTRL-V| | ||||||
|  |  | ||||||
|  |                     If {fn} is `nil`, it removes the callback for the | ||||||
|  |                     associated {ns_id}. | ||||||
|  |  | ||||||
|  |             {ns_id}: (number)  Namespace ID. If not passed or 0, will generate | ||||||
|  |                      and return a new namespace ID from |nvim_create_namespace()| | ||||||
|  |  | ||||||
|  |         Return: ~ | ||||||
|  |             (number) Namespace ID associated with {fn} | ||||||
|  |  | ||||||
|  |         NOTE: {fn} will be automatically removed if an error occurs while | ||||||
|  |         calling. This is to prevent the annoying situation of every keystroke | ||||||
|  |         erroring while trying to remove a broken callback. | ||||||
|  |  | ||||||
|  |         NOTE: {fn} will receive the keystrokes after mappings have been | ||||||
|  |         evaluated | ||||||
|  |  | ||||||
|  |         NOTE: {fn} will *NOT* be cleared from |nvim_buf_clear_namespace()| | ||||||
|  |  | ||||||
| vim.rpcnotify({channel}, {method}[, {args}...])		    *vim.rpcnotify()* | vim.rpcnotify({channel}, {method}[, {args}...])		    *vim.rpcnotify()* | ||||||
| 	Sends {event} to {channel} via |RPC| and returns immediately. | 	Sends {event} to {channel} via |RPC| and returns immediately. | ||||||
| 	If {channel} is 0, the event is broadcast to all channels. | 	If {channel} is 0, the event is broadcast to all channels. | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ | |||||||
| #include "nvim/ex_docmd.h" | #include "nvim/ex_docmd.h" | ||||||
| #include "nvim/ex_getln.h" | #include "nvim/ex_getln.h" | ||||||
| #include "nvim/func_attr.h" | #include "nvim/func_attr.h" | ||||||
|  | #include "nvim/lua/executor.h" | ||||||
| #include "nvim/main.h" | #include "nvim/main.h" | ||||||
| #include "nvim/mbyte.h" | #include "nvim/mbyte.h" | ||||||
| #include "nvim/memline.h" | #include "nvim/memline.h" | ||||||
| @@ -1535,6 +1536,9 @@ int vgetc(void) | |||||||
|    */ |    */ | ||||||
|   may_garbage_collect = false; |   may_garbage_collect = false; | ||||||
|  |  | ||||||
|  |   // Exec lua callbacks for on_keystroke | ||||||
|  |   nlua_execute_log_keystroke(c); | ||||||
|  |  | ||||||
|   return c; |   return c; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -530,13 +530,24 @@ unsigned int trans_special(const char_u **srcp, const size_t src_len, | |||||||
| { | { | ||||||
|   int modifiers = 0; |   int modifiers = 0; | ||||||
|   int key; |   int key; | ||||||
|   unsigned int dlen = 0; |  | ||||||
|  |  | ||||||
|   key = find_special_key(srcp, src_len, &modifiers, keycode, false, in_string); |   key = find_special_key(srcp, src_len, &modifiers, keycode, false, in_string); | ||||||
|   if (key == 0) { |   if (key == 0) { | ||||||
|     return 0; |     return 0; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   return special_to_buf(key, modifiers, keycode, dst); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Put the character sequence for "key" with "modifiers" into "dst" and return | ||||||
|  | /// the resulting length. | ||||||
|  | /// When "keycode" is TRUE prefer key code, e.g. K_DEL instead of DEL. | ||||||
|  | /// The sequence is not NUL terminated. | ||||||
|  | /// This is how characters in a string are encoded. | ||||||
|  | unsigned int special_to_buf(int key, int modifiers, bool keycode, char_u *dst) | ||||||
|  | { | ||||||
|  |   unsigned int dlen = 0; | ||||||
|  |  | ||||||
|   // Put the appropriate modifier in a string. |   // Put the appropriate modifier in a string. | ||||||
|   if (modifiers != 0) { |   if (modifiers != 0) { | ||||||
|     dst[dlen++] = K_SPECIAL; |     dst[dlen++] = K_SPECIAL; | ||||||
|   | |||||||
| @@ -1465,3 +1465,40 @@ void nlua_free_typval_dict(dict_T *const d) | |||||||
|     d->lua_table_ref = LUA_NOREF; |     d->lua_table_ref = LUA_NOREF; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void nlua_execute_log_keystroke(int c) | ||||||
|  | { | ||||||
|  |   char_u buf[NUMBUFLEN]; | ||||||
|  |   size_t buf_len = special_to_buf(c, mod_mask, false, buf); | ||||||
|  |  | ||||||
|  |   lua_State *const lstate = nlua_enter(); | ||||||
|  |  | ||||||
|  | #ifndef NDEBUG | ||||||
|  |   int top = lua_gettop(lstate); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   // [ vim ] | ||||||
|  |   lua_getglobal(lstate, "vim"); | ||||||
|  |  | ||||||
|  |   // [ vim, vim._log_keystroke ] | ||||||
|  |   lua_getfield(lstate, -1, "_log_keystroke"); | ||||||
|  |   luaL_checktype(lstate, -1, LUA_TFUNCTION); | ||||||
|  |  | ||||||
|  |   // [ vim, vim._log_keystroke, buf ] | ||||||
|  |   lua_pushlstring(lstate, (const char *)buf, buf_len); | ||||||
|  |  | ||||||
|  |   if (lua_pcall(lstate, 1, 0, 0)) { | ||||||
|  |     nlua_error( | ||||||
|  |         lstate, | ||||||
|  |         _("Error executing vim.log_keystroke lua callback: %.*s")); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // [ vim ] | ||||||
|  |   lua_pop(lstate, 1); | ||||||
|  |  | ||||||
|  | #ifndef NDEBUG | ||||||
|  |   // [ ] | ||||||
|  |   assert(top == lua_gettop(lstate)); | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -489,4 +489,60 @@ function vim.defer_fn(fn, timeout) | |||||||
|   return timer |   return timer | ||||||
| end | end | ||||||
|  |  | ||||||
|  | local on_keystroke_callbacks = {} | ||||||
|  |  | ||||||
|  | --- Register a lua {fn} with an {id} to be run after every keystroke. | ||||||
|  | --- | ||||||
|  | --@param fn function: Function to call. It should take one argument, which is a string. | ||||||
|  | ---                   The string will contain the literal keys typed. | ||||||
|  | ---                   See |i_CTRL-V| | ||||||
|  | --- | ||||||
|  | ---                   If {fn} is nil, it removes the callback for the associated {ns_id} | ||||||
|  | --@param ns_id number? Namespace ID. If not passed or 0, will generate and return a new | ||||||
|  | ---                    namespace ID from |nvim_create_namesapce()| | ||||||
|  | --- | ||||||
|  | --@return number Namespace ID associated with {fn} | ||||||
|  | --- | ||||||
|  | --@note {fn} will be automatically removed if an error occurs while calling. | ||||||
|  | ---     This is to prevent the annoying situation of every keystroke erroring | ||||||
|  | ---     while trying to remove a broken callback. | ||||||
|  | --@note {fn} will not be cleared from |nvim_buf_clear_namespace()| | ||||||
|  | --@note {fn} will receive the keystrokes after mappings have been evaluated | ||||||
|  | function vim.register_keystroke_callback(fn, ns_id) | ||||||
|  |   vim.validate { | ||||||
|  |     fn = { fn, 'c', true}, | ||||||
|  |     ns_id = { ns_id, 'n', true } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if ns_id == nil or ns_id == 0 then | ||||||
|  |     ns_id = vim.api.nvim_create_namespace('') | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   on_keystroke_callbacks[ns_id] = fn | ||||||
|  |   return ns_id | ||||||
|  | end | ||||||
|  |  | ||||||
|  | --- Function that executes the keystroke callbacks. | ||||||
|  | --@private | ||||||
|  | function vim._log_keystroke(char) | ||||||
|  |   local failed_ns_ids = {} | ||||||
|  |   local failed_messages = {} | ||||||
|  |   for k, v in pairs(on_keystroke_callbacks) do | ||||||
|  |     local ok, err_msg = pcall(v, char) | ||||||
|  |     if not ok then | ||||||
|  |       vim.register_keystroke_callback(nil, k) | ||||||
|  |  | ||||||
|  |       table.insert(failed_ns_ids, k) | ||||||
|  |       table.insert(failed_messages, err_msg) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   if failed_ns_ids[1] then | ||||||
|  |     error(string.format( | ||||||
|  |       "Error executing 'on_keystroke' with ns_ids of '%s'\n    With messages: %s", | ||||||
|  |       table.concat(failed_ns_ids, ", "), | ||||||
|  |       table.concat(failed_messages, "\n"))) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  |  | ||||||
| return module | return module | ||||||
|   | |||||||
| @@ -1068,6 +1068,104 @@ describe('lua stdlib', function() | |||||||
|     eq({5,15}, exec_lua[[ return vim.region(0,{1,5},{1,14},'v',true)[1] ]]) |     eq({5,15}, exec_lua[[ return vim.region(0,{1,5},{1,14},'v',true)[1] ]]) | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
|  |   describe('vim.execute_on_keystroke', function() | ||||||
|  |     it('should keep track of keystrokes', function() | ||||||
|  |       helpers.insert([[hello world ]]) | ||||||
|  |  | ||||||
|  |       exec_lua [[ | ||||||
|  |         KeysPressed = {} | ||||||
|  |  | ||||||
|  |         vim.register_keystroke_callback(function(buf) | ||||||
|  |           if buf:byte() == 27 then | ||||||
|  |             buf = "<ESC>" | ||||||
|  |           end | ||||||
|  |  | ||||||
|  |           table.insert(KeysPressed, buf) | ||||||
|  |         end) | ||||||
|  |       ]] | ||||||
|  |  | ||||||
|  |       helpers.insert([[next 🤦 lines å ]]) | ||||||
|  |  | ||||||
|  |       -- It has escape in the keys pressed | ||||||
|  |       eq('inext 🤦 lines å <ESC>', exec_lua [[return table.concat(KeysPressed, '')]]) | ||||||
|  |     end) | ||||||
|  |  | ||||||
|  |     it('should allow removing trackers.', function() | ||||||
|  |       helpers.insert([[hello world]]) | ||||||
|  |  | ||||||
|  |       exec_lua [[ | ||||||
|  |         KeysPressed = {} | ||||||
|  |  | ||||||
|  |         return vim.register_keystroke_callback(function(buf) | ||||||
|  |           if buf:byte() == 27 then | ||||||
|  |             buf = "<ESC>" | ||||||
|  |           end | ||||||
|  |  | ||||||
|  |           table.insert(KeysPressed, buf) | ||||||
|  |         end, vim.api.nvim_create_namespace("logger")) | ||||||
|  |       ]] | ||||||
|  |  | ||||||
|  |       helpers.insert([[next lines]]) | ||||||
|  |  | ||||||
|  |       exec_lua("vim.register_keystroke_callback(nil, vim.api.nvim_create_namespace('logger'))") | ||||||
|  |  | ||||||
|  |       helpers.insert([[more lines]]) | ||||||
|  |  | ||||||
|  |       -- It has escape in the keys pressed | ||||||
|  |       eq('inext lines<ESC>', exec_lua [[return table.concat(KeysPressed, '')]]) | ||||||
|  |     end) | ||||||
|  |  | ||||||
|  |     it('should not call functions that error again.', function() | ||||||
|  |       helpers.insert([[hello world]]) | ||||||
|  |  | ||||||
|  |       exec_lua [[ | ||||||
|  |         KeysPressed = {} | ||||||
|  |  | ||||||
|  |         return vim.register_keystroke_callback(function(buf) | ||||||
|  |           if buf:byte() == 27 then | ||||||
|  |             buf = "<ESC>" | ||||||
|  |           end | ||||||
|  |  | ||||||
|  |           table.insert(KeysPressed, buf) | ||||||
|  |  | ||||||
|  |           if buf == 'l' then | ||||||
|  |             error("Dumb Error") | ||||||
|  |           end | ||||||
|  |         end) | ||||||
|  |       ]] | ||||||
|  |  | ||||||
|  |       helpers.insert([[next lines]]) | ||||||
|  |       helpers.insert([[more lines]]) | ||||||
|  |  | ||||||
|  |       -- Only the first letter gets added. After that we remove the callback | ||||||
|  |       eq('inext l', exec_lua [[ return table.concat(KeysPressed, '') ]]) | ||||||
|  |     end) | ||||||
|  |  | ||||||
|  |     it('should process mapped keys, not unmapped keys', function() | ||||||
|  |       exec_lua [[ | ||||||
|  |         KeysPressed = {} | ||||||
|  |  | ||||||
|  |         vim.cmd("inoremap hello world") | ||||||
|  |  | ||||||
|  |         vim.register_keystroke_callback(function(buf) | ||||||
|  |           if buf:byte() == 27 then | ||||||
|  |             buf = "<ESC>" | ||||||
|  |           end | ||||||
|  |  | ||||||
|  |           table.insert(KeysPressed, buf) | ||||||
|  |         end) | ||||||
|  |       ]] | ||||||
|  |  | ||||||
|  |       helpers.insert("hello") | ||||||
|  |  | ||||||
|  |       local next_status = exec_lua [[ | ||||||
|  |         return table.concat(KeysPressed, '') | ||||||
|  |       ]] | ||||||
|  |  | ||||||
|  |       eq("iworld<ESC>", next_status) | ||||||
|  |     end) | ||||||
|  |   end) | ||||||
|  |  | ||||||
|   describe('vim.wait', function() |   describe('vim.wait', function() | ||||||
|     before_each(function() |     before_each(function() | ||||||
|       exec_lua[[ |       exec_lua[[ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 TJ DeVries
					TJ DeVries