mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	api: allow nvim_buf_attach from lua using callbacks
This commit is contained in:
		| @@ -199,6 +199,21 @@ paste a block of 6 lines, emits: > | |||||||
| User reloads the buffer with ":edit", emits: > | User reloads the buffer with ":edit", emits: > | ||||||
|   nvim_buf_detach_event[{buf}] |   nvim_buf_detach_event[{buf}] | ||||||
|  |  | ||||||
|  |                                                         *api-buffer-updates-lua* | ||||||
|  | In-process lua plugins can also recieve buffer updates, in the form of lua | ||||||
|  | callbacks. These callbacks are called frequently in various contexts, buffer | ||||||
|  | contents or window layout should not be changed inside these |textlock|. | ||||||
|  |  | ||||||
|  | |nvim_buf_attach| will take keyword args for the callbacks. "on_lines" will | ||||||
|  | receive parameters ("lines", {buf}, {changedtick}, {firstline}, {lastline}, {new_lastline}). | ||||||
|  | Unlike remote channels the text contents are not passed. The new text can be | ||||||
|  | accessed inside the callback as | ||||||
|  | `vim.api.nvim_buf_get_lines(buf, firstline, new_lastline, true)` | ||||||
|  | "on_changedtick" is invoked when |b:changedtick| was incremented but no text | ||||||
|  | was changed. The parameters recieved are ("changedtick", {buf}, {changedtick}). | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ============================================================================== | ============================================================================== | ||||||
| Buffer highlighting					       *api-highlights* | Buffer highlighting					       *api-highlights* | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2539,6 +2539,8 @@ def CheckSpacing(filename, clean_lines, linenum, nesting_state, error): | |||||||
|                    r'(?<!\bkbtree_t)' |                    r'(?<!\bkbtree_t)' | ||||||
|                    r'(?<!\bkbitr_t)' |                    r'(?<!\bkbitr_t)' | ||||||
|                    r'(?<!\bPMap)' |                    r'(?<!\bPMap)' | ||||||
|  |                    r'(?<!\bArrayOf)' | ||||||
|  |                    r'(?<!\bDictionaryOf)' | ||||||
|                    r'\((?:const )?(?:struct )?[a-zA-Z_]\w*(?: *\*(?:const)?)*\)' |                    r'\((?:const )?(?:struct )?[a-zA-Z_]\w*(?: *\*(?:const)?)*\)' | ||||||
|                    r' +' |                    r' +' | ||||||
|                    r'-?(?:\*+|&)?(?:\w+|\+\+|--|\()', cast_line) |                    r'-?(?:\*+|&)?(?:\w+|\+\+|--|\()', cast_line) | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ | |||||||
| #include <stdint.h> | #include <stdint.h> | ||||||
| #include <stdlib.h> | #include <stdlib.h> | ||||||
| #include <limits.h> | #include <limits.h> | ||||||
|  | #include <lauxlib.h> | ||||||
|  |  | ||||||
| #include "nvim/api/buffer.h" | #include "nvim/api/buffer.h" | ||||||
| #include "nvim/api/private/helpers.h" | #include "nvim/api/private/helpers.h" | ||||||
| @@ -98,37 +99,62 @@ String buffer_get_line(Buffer buffer, Integer index, Error *err) | |||||||
|   return rv; |   return rv; | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Activates buffer-update events on the channel. | /// Activates buffer-update events on a channel, or as lua callbacks. | ||||||
| /// | /// | ||||||
| /// @param channel_id | /// @param channel_id | ||||||
| /// @param buffer Buffer handle, or 0 for current buffer | /// @param buffer Buffer handle, or 0 for current buffer | ||||||
| /// @param send_buffer Set to true if the initial notification should contain | /// @param send_buffer Set to true if the initial notification should contain | ||||||
| ///        the whole buffer. If so, the first notification will be a | ///        the whole buffer. If so, the first notification will be a | ||||||
| ///        `nvim_buf_lines_event`. Otherwise, the first notification will be | ///        `nvim_buf_lines_event`. Otherwise, the first notification will be | ||||||
| ///        a `nvim_buf_changedtick_event` | ///        a `nvim_buf_changedtick_event`. Not used for lua callbacks. | ||||||
| /// @param  opts  Optional parameters. Reserved for future use. | /// @param  opts  Optional parameters. | ||||||
|  | ///               `on_lines`: lua callback received on change. | ||||||
|  | ///               `on_changedtick`: lua callback received on changedtick | ||||||
|  | ///                                 increment without text change. | ||||||
|  | ///               See |api-buffer-updates-lua| for more information | ||||||
| /// @param[out] err Error details, if any | /// @param[out] err Error details, if any | ||||||
| /// @return False when updates couldn't be enabled because the buffer isn't | /// @return False when updates couldn't be enabled because the buffer isn't | ||||||
| ///         loaded or `opts` contained an invalid key; otherwise True. | ///         loaded or `opts` contained an invalid key; otherwise True. | ||||||
|  | ///         TODO: LUA_API_NO_EVAL | ||||||
| Boolean nvim_buf_attach(uint64_t channel_id, | Boolean nvim_buf_attach(uint64_t channel_id, | ||||||
|                         Buffer buffer, |                         Buffer buffer, | ||||||
|                         Boolean send_buffer, |                         Boolean send_buffer, | ||||||
|                         Dictionary opts, |                         DictionaryOf(LuaRef) opts, | ||||||
|                         Error *err) |                         Error *err) | ||||||
|   FUNC_API_SINCE(4) FUNC_API_REMOTE_ONLY |   FUNC_API_SINCE(4) | ||||||
| { | { | ||||||
|   if (opts.size > 0) { |  | ||||||
|       api_set_error(err, kErrorTypeValidation, "dict isn't empty"); |  | ||||||
|       return false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   buf_T *buf = find_buffer_by_handle(buffer, err); |   buf_T *buf = find_buffer_by_handle(buffer, err); | ||||||
|  |  | ||||||
|   if (!buf) { |   if (!buf) { | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return buf_updates_register(buf, channel_id, send_buffer); |   bool is_lua = (channel_id == LUA_INTERNAL_CALL); | ||||||
|  |   BufUpdateCallbacks cb = BUF_UPDATE_CALLBACKS_INIT; | ||||||
|  |   for (size_t i = 0; i < opts.size; i++) { | ||||||
|  |     String k = opts.items[i].key; | ||||||
|  |     Object *v = &opts.items[i].value; | ||||||
|  |     if (is_lua && strequal("on_lines", k.data)) { | ||||||
|  |       if (v->type != kObjectTypeLuaRef) { | ||||||
|  |         api_set_error(err, kErrorTypeValidation, "callback is not a function"); | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       cb.on_lines = v->data.luaref; | ||||||
|  |       v->data.integer = LUA_NOREF; | ||||||
|  |     } else if (is_lua && strequal("on_changedtick", k.data)) { | ||||||
|  |       if (v->type != kObjectTypeLuaRef) { | ||||||
|  |         api_set_error(err, kErrorTypeValidation, "callback is not a function"); | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       cb.on_changedtick = v->data.luaref; | ||||||
|  |       v->data.integer = LUA_NOREF; | ||||||
|  |     } else { | ||||||
|  |       api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return buf_updates_register(buf, channel_id, cb, send_buffer); | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Deactivates buffer-update events on the channel. | /// Deactivates buffer-update events on the channel. | ||||||
| @@ -307,7 +333,7 @@ void buffer_set_line_slice(Buffer buffer, | |||||||
|                            Integer end, |                            Integer end, | ||||||
|                            Boolean include_start, |                            Boolean include_start, | ||||||
|                            Boolean include_end, |                            Boolean include_end, | ||||||
|                            ArrayOf(String) replacement,  // NOLINT |                            ArrayOf(String) replacement, | ||||||
|                            Error *err) |                            Error *err) | ||||||
| { | { | ||||||
|   start = convert_index(start) + !include_start; |   start = convert_index(start) + !include_start; | ||||||
| @@ -340,7 +366,7 @@ void nvim_buf_set_lines(uint64_t channel_id, | |||||||
|                         Integer start, |                         Integer start, | ||||||
|                         Integer end, |                         Integer end, | ||||||
|                         Boolean strict_indexing, |                         Boolean strict_indexing, | ||||||
|                         ArrayOf(String) replacement,  // NOLINT |                         ArrayOf(String) replacement, | ||||||
|                         Error *err) |                         Error *err) | ||||||
|   FUNC_API_SINCE(1) |   FUNC_API_SINCE(1) | ||||||
| { | { | ||||||
|   | |||||||
| @@ -104,6 +104,7 @@ typedef enum { | |||||||
|   kObjectTypeString, |   kObjectTypeString, | ||||||
|   kObjectTypeArray, |   kObjectTypeArray, | ||||||
|   kObjectTypeDictionary, |   kObjectTypeDictionary, | ||||||
|  |   kObjectTypeLuaRef, | ||||||
|   // EXT types, cannot be split or reordered, see #EXT_OBJECT_TYPE_SHIFT |   // EXT types, cannot be split or reordered, see #EXT_OBJECT_TYPE_SHIFT | ||||||
|   kObjectTypeBuffer, |   kObjectTypeBuffer, | ||||||
|   kObjectTypeWindow, |   kObjectTypeWindow, | ||||||
| @@ -119,6 +120,7 @@ struct object { | |||||||
|     String string; |     String string; | ||||||
|     Array array; |     Array array; | ||||||
|     Dictionary dictionary; |     Dictionary dictionary; | ||||||
|  |     LuaRef luaref; | ||||||
|   } data; |   } data; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ | |||||||
| #include "nvim/api/private/defs.h" | #include "nvim/api/private/defs.h" | ||||||
| #include "nvim/api/private/handle.h" | #include "nvim/api/private/handle.h" | ||||||
| #include "nvim/msgpack_rpc/helpers.h" | #include "nvim/msgpack_rpc/helpers.h" | ||||||
|  | #include "nvim/lua/executor.h" | ||||||
| #include "nvim/ascii.h" | #include "nvim/ascii.h" | ||||||
| #include "nvim/assert.h" | #include "nvim/assert.h" | ||||||
| #include "nvim/vim.h" | #include "nvim/vim.h" | ||||||
| @@ -1147,6 +1148,10 @@ void api_free_object(Object value) | |||||||
|       api_free_dictionary(value.data.dictionary); |       api_free_dictionary(value.data.dictionary); | ||||||
|       break; |       break; | ||||||
|  |  | ||||||
|  |     case kObjectTypeLuaRef: | ||||||
|  |       executor_free_luaref(value.data.luaref); | ||||||
|  |       break; | ||||||
|  |  | ||||||
|     default: |     default: | ||||||
|       abort(); |       abort(); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -48,6 +48,10 @@ | |||||||
|     .type = kObjectTypeDictionary, \ |     .type = kObjectTypeDictionary, \ | ||||||
|     .data.dictionary = d }) |     .data.dictionary = d }) | ||||||
|  |  | ||||||
|  | #define LUAREF_OBJ(r) ((Object) { \ | ||||||
|  |     .type = kObjectTypeLuaRef, \ | ||||||
|  |     .data.luaref = r }) | ||||||
|  |  | ||||||
| #define NIL ((Object) {.type = kObjectTypeNil}) | #define NIL ((Object) {.type = kObjectTypeNil}) | ||||||
|  |  | ||||||
| #define PUT(dict, k, v) \ | #define PUT(dict, k, v) \ | ||||||
|   | |||||||
| @@ -1836,6 +1836,8 @@ buf_T * buflist_new(char_u *ffname, char_u *sfname, linenr_T lnum, int flags) | |||||||
|   buf->b_p_bl = (flags & BLN_LISTED) ? true : false;    // init 'buflisted' |   buf->b_p_bl = (flags & BLN_LISTED) ? true : false;    // init 'buflisted' | ||||||
|   kv_destroy(buf->update_channels); |   kv_destroy(buf->update_channels); | ||||||
|   kv_init(buf->update_channels); |   kv_init(buf->update_channels); | ||||||
|  |   kv_destroy(buf->update_callbacks); | ||||||
|  |   kv_init(buf->update_callbacks); | ||||||
|   if (!(flags & BLN_DUMMY)) { |   if (!(flags & BLN_DUMMY)) { | ||||||
|     // Tricky: these autocommands may change the buffer list.  They could also |     // Tricky: these autocommands may change the buffer list.  They could also | ||||||
|     // split the window with re-using the one empty buffer. This may result in |     // split the window with re-using the one empty buffer. This may result in | ||||||
|   | |||||||
| @@ -453,6 +453,12 @@ typedef struct { | |||||||
| /// Primary exists so that literals of relevant type can be made. | /// Primary exists so that literals of relevant type can be made. | ||||||
| typedef TV_DICTITEM_STRUCT(sizeof("changedtick")) ChangedtickDictItem; | typedef TV_DICTITEM_STRUCT(sizeof("changedtick")) ChangedtickDictItem; | ||||||
|  |  | ||||||
|  | typedef struct { | ||||||
|  |   LuaRef on_lines; | ||||||
|  |   LuaRef on_changedtick; | ||||||
|  | } BufUpdateCallbacks; | ||||||
|  | #define BUF_UPDATE_CALLBACKS_INIT { LUA_NOREF, LUA_NOREF } | ||||||
|  |  | ||||||
| #define BUF_HAS_QF_ENTRY 1 | #define BUF_HAS_QF_ENTRY 1 | ||||||
| #define BUF_HAS_LL_ENTRY 2 | #define BUF_HAS_LL_ENTRY 2 | ||||||
|  |  | ||||||
| @@ -796,6 +802,7 @@ struct file_buffer { | |||||||
|   // array of channelids which have asked to receive updates for this |   // array of channelids which have asked to receive updates for this | ||||||
|   // buffer. |   // buffer. | ||||||
|   kvec_t(uint64_t) update_channels; |   kvec_t(uint64_t) update_channels; | ||||||
|  |   kvec_t(BufUpdateCallbacks) update_callbacks; | ||||||
|  |  | ||||||
|   int b_diff_failed;    // internal diff failed for this buffer |   int b_diff_failed;    // internal diff failed for this buffer | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -5,19 +5,30 @@ | |||||||
| #include "nvim/memline.h" | #include "nvim/memline.h" | ||||||
| #include "nvim/api/private/helpers.h" | #include "nvim/api/private/helpers.h" | ||||||
| #include "nvim/msgpack_rpc/channel.h" | #include "nvim/msgpack_rpc/channel.h" | ||||||
|  | #include "nvim/lua/executor.h" | ||||||
| #include "nvim/assert.h" | #include "nvim/assert.h" | ||||||
| #include "nvim/buffer.h" | #include "nvim/buffer.h" | ||||||
|  |  | ||||||
|  | #ifdef INCLUDE_GENERATED_DECLARATIONS | ||||||
|  | # include "buffer_updates.c.generated.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
| // Register a channel. Return True if the channel was added, or already added. | // Register a channel. Return True if the channel was added, or already added. | ||||||
| // Return False if the channel couldn't be added because the buffer is | // Return False if the channel couldn't be added because the buffer is | ||||||
| // unloaded. | // unloaded. | ||||||
| bool buf_updates_register(buf_T *buf, uint64_t channel_id, bool send_buffer) | bool buf_updates_register(buf_T *buf, uint64_t channel_id, | ||||||
|  |                           BufUpdateCallbacks cb, bool send_buffer) | ||||||
| { | { | ||||||
|   // must fail if the buffer isn't loaded |   // must fail if the buffer isn't loaded | ||||||
|   if (buf->b_ml.ml_mfp == NULL) { |   if (buf->b_ml.ml_mfp == NULL) { | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   if (channel_id == LUA_INTERNAL_CALL) { | ||||||
|  |     kv_push(buf->update_callbacks, cb); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // count how many channels are currently watching the buffer |   // count how many channels are currently watching the buffer | ||||||
|   size_t size = kv_size(buf->update_channels); |   size_t size = kv_size(buf->update_channels); | ||||||
|   if (size) { |   if (size) { | ||||||
| @@ -69,6 +80,11 @@ bool buf_updates_register(buf_T *buf, uint64_t channel_id, bool send_buffer) | |||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | bool buf_updates_active(buf_T *buf) | ||||||
|  | { | ||||||
|  |     return kv_size(buf->update_channels) || kv_size(buf->update_callbacks); | ||||||
|  | } | ||||||
|  |  | ||||||
| void buf_updates_send_end(buf_T *buf, uint64_t channelid) | void buf_updates_send_end(buf_T *buf, uint64_t channelid) | ||||||
| { | { | ||||||
|     Array args = ARRAY_DICT_INIT; |     Array args = ARRAY_DICT_INIT; | ||||||
| @@ -125,6 +141,12 @@ void buf_updates_unregister_all(buf_T *buf) | |||||||
|     kv_destroy(buf->update_channels); |     kv_destroy(buf->update_channels); | ||||||
|     kv_init(buf->update_channels); |     kv_init(buf->update_channels); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   for (size_t i = 0; i < kv_size(buf->update_callbacks); i++) { | ||||||
|  |     free_update_callbacks(kv_A(buf->update_callbacks, i)); | ||||||
|  |   } | ||||||
|  |   kv_destroy(buf->update_callbacks); | ||||||
|  |   kv_init(buf->update_callbacks); | ||||||
| } | } | ||||||
|  |  | ||||||
| void buf_updates_send_changes(buf_T *buf, | void buf_updates_send_changes(buf_T *buf, | ||||||
| @@ -133,6 +155,10 @@ void buf_updates_send_changes(buf_T *buf, | |||||||
|                               int64_t num_removed, |                               int64_t num_removed, | ||||||
|                               bool send_tick) |                               bool send_tick) | ||||||
| { | { | ||||||
|  |   if (!buf_updates_active(buf)) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // if one the channels doesn't work, put its ID here so we can remove it later |   // if one the channels doesn't work, put its ID here so we can remove it later | ||||||
|   uint64_t badchannelid = 0; |   uint64_t badchannelid = 0; | ||||||
|  |  | ||||||
| @@ -183,6 +209,47 @@ void buf_updates_send_changes(buf_T *buf, | |||||||
|     ELOG("Disabling buffer updates for dead channel %"PRIu64, badchannelid); |     ELOG("Disabling buffer updates for dead channel %"PRIu64, badchannelid); | ||||||
|     buf_updates_unregister(buf, badchannelid); |     buf_updates_unregister(buf, badchannelid); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // notify each of the active channels | ||||||
|  |   size_t j = 0; | ||||||
|  |   for (size_t i = 0; i < kv_size(buf->update_callbacks); i++) { | ||||||
|  |     BufUpdateCallbacks cb = kv_A(buf->update_callbacks, i); | ||||||
|  |     bool keep = true; | ||||||
|  |     if (cb.on_lines != LUA_NOREF) { | ||||||
|  |       Array args = ARRAY_DICT_INIT; | ||||||
|  |       Object items[5]; | ||||||
|  |       args.size = 5; | ||||||
|  |       args.items = items; | ||||||
|  |  | ||||||
|  |       // the first argument is always the buffer handle | ||||||
|  |       args.items[0] = BUFFER_OBJ(buf->handle); | ||||||
|  |  | ||||||
|  |       // next argument is b:changedtick | ||||||
|  |       args.items[1] = send_tick ? INTEGER_OBJ(buf_get_changedtick(buf)) : NIL; | ||||||
|  |  | ||||||
|  |       // the first line that changed (zero-indexed) | ||||||
|  |       args.items[2] = INTEGER_OBJ(firstline - 1); | ||||||
|  |  | ||||||
|  |       // the last line that was changed | ||||||
|  |       args.items[3] = INTEGER_OBJ(firstline - 1 + num_removed); | ||||||
|  |  | ||||||
|  |       // the last line in the updated range | ||||||
|  |       args.items[4] = INTEGER_OBJ(firstline - 1 + num_added); | ||||||
|  |  | ||||||
|  |       textlock++; | ||||||
|  |       Object res = executor_exec_lua_cb(cb.on_lines, "lines", args); | ||||||
|  |       textlock--; | ||||||
|  |  | ||||||
|  |       if (res.type == kObjectTypeBoolean && res.data.boolean == true) { | ||||||
|  |         free_update_callbacks(cb); | ||||||
|  |         keep = false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (keep) { | ||||||
|  |       kv_A(buf->update_callbacks, j++) = kv_A(buf->update_callbacks, i); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   kv_size(buf->update_callbacks) = j; | ||||||
| } | } | ||||||
|  |  | ||||||
| void buf_updates_changedtick(buf_T *buf) | void buf_updates_changedtick(buf_T *buf) | ||||||
| @@ -192,6 +259,36 @@ void buf_updates_changedtick(buf_T *buf) | |||||||
|     uint64_t channel_id = kv_A(buf->update_channels, i); |     uint64_t channel_id = kv_A(buf->update_channels, i); | ||||||
|     buf_updates_changedtick_single(buf, channel_id); |     buf_updates_changedtick_single(buf, channel_id); | ||||||
|   } |   } | ||||||
|  |   size_t j = 0; | ||||||
|  |   for (size_t i = 0; i < kv_size(buf->update_callbacks); i++) { | ||||||
|  |     BufUpdateCallbacks cb = kv_A(buf->update_callbacks, i); | ||||||
|  |     bool keep = true; | ||||||
|  |     if (cb.on_changedtick != LUA_NOREF) { | ||||||
|  |       Array args = ARRAY_DICT_INIT; | ||||||
|  |       Object items[2]; | ||||||
|  |       args.size = 2; | ||||||
|  |       args.items = items; | ||||||
|  |  | ||||||
|  |       // the first argument is always the buffer handle | ||||||
|  |       args.items[0] = BUFFER_OBJ(buf->handle); | ||||||
|  |  | ||||||
|  |       // next argument is b:changedtick | ||||||
|  |       args.items[1] = INTEGER_OBJ(buf_get_changedtick(buf)); | ||||||
|  |  | ||||||
|  |       textlock++; | ||||||
|  |       Object res = executor_exec_lua_cb(cb.on_changedtick, "changedtick", args); | ||||||
|  |       textlock--; | ||||||
|  |  | ||||||
|  |       if (res.type == kObjectTypeBoolean && res.data.boolean == true) { | ||||||
|  |         free_update_callbacks(cb); | ||||||
|  |         keep = false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (keep) { | ||||||
|  |       kv_A(buf->update_callbacks, j++) = kv_A(buf->update_callbacks, i); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   kv_size(buf->update_callbacks) = j; | ||||||
| } | } | ||||||
|  |  | ||||||
| void buf_updates_changedtick_single(buf_T *buf, uint64_t channel_id) | void buf_updates_changedtick_single(buf_T *buf, uint64_t channel_id) | ||||||
| @@ -209,3 +306,9 @@ void buf_updates_changedtick_single(buf_T *buf, uint64_t channel_id) | |||||||
|     // don't try and clean up dead channels here |     // don't try and clean up dead channels here | ||||||
|     rpc_send_event(channel_id, "nvim_buf_changedtick_event", args); |     rpc_send_event(channel_id, "nvim_buf_changedtick_event", args); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | static void free_update_callbacks(BufUpdateCallbacks cb) | ||||||
|  | { | ||||||
|  |   executor_free_luaref(cb.on_lines); | ||||||
|  |   executor_free_luaref(cb.on_changedtick); | ||||||
|  | } | ||||||
|   | |||||||
| @@ -900,9 +900,7 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest) | |||||||
|   changed_lines(last_line - num_lines + 1, 0, last_line + 1, -extra, false); |   changed_lines(last_line - num_lines + 1, 0, last_line + 1, -extra, false); | ||||||
|  |  | ||||||
|   // send update regarding the new lines that were added |   // send update regarding the new lines that were added | ||||||
|   if (kv_size(curbuf->update_channels)) { |  | ||||||
|   buf_updates_send_changes(curbuf, dest + 1, num_lines, 0, true); |   buf_updates_send_changes(curbuf, dest + 1, num_lines, 0, true); | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * Now we delete the original text -- webb |    * Now we delete the original text -- webb | ||||||
| @@ -939,9 +937,7 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest) | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   // send nvim_buf_lines_event regarding lines that were deleted |   // send nvim_buf_lines_event regarding lines that were deleted | ||||||
|   if (kv_size(curbuf->update_channels)) { |  | ||||||
|   buf_updates_send_changes(curbuf, line1 + extra, 0, num_lines, true); |   buf_updates_send_changes(curbuf, line1 + extra, 0, num_lines, true); | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return OK; |   return OK; | ||||||
| } | } | ||||||
| @@ -4074,13 +4070,11 @@ skip: | |||||||
|     i = curbuf->b_ml.ml_line_count - old_line_count; |     i = curbuf->b_ml.ml_line_count - old_line_count; | ||||||
|     changed_lines(first_line, 0, last_line - i, i, false); |     changed_lines(first_line, 0, last_line - i, i, false); | ||||||
|  |  | ||||||
|     if (kv_size(curbuf->update_channels)) { |  | ||||||
|     int64_t num_added = last_line - first_line; |     int64_t num_added = last_line - first_line; | ||||||
|     int64_t num_removed = num_added - i; |     int64_t num_removed = num_added - i; | ||||||
|     buf_updates_send_changes(curbuf, first_line, num_added, num_removed, |     buf_updates_send_changes(curbuf, first_line, num_added, num_removed, | ||||||
|                              do_buf_event); |                              do_buf_event); | ||||||
|   } |   } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   xfree(sub_firstline);   /* may have to free allocated copy of the line */ |   xfree(sub_firstline);   /* may have to free allocated copy of the line */ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -737,7 +737,6 @@ void deleteFold( | |||||||
|     changed_lines(first_lnum, (colnr_T)0, last_lnum, 0L, false); |     changed_lines(first_lnum, (colnr_T)0, last_lnum, 0L, false); | ||||||
|  |  | ||||||
|     // send one nvim_buf_lines_event at the end |     // send one nvim_buf_lines_event at the end | ||||||
|     if (kv_size(curbuf->update_channels)) { |  | ||||||
|     // last_lnum is the line *after* the last line of the outermost fold |     // last_lnum is the line *after* the last line of the outermost fold | ||||||
|     // that was modified. Note also that deleting a fold might only require |     // that was modified. Note also that deleting a fold might only require | ||||||
|     // the modification of the *first* line of the fold, but we send through a |     // the modification of the *first* line of the fold, but we send through a | ||||||
| @@ -746,7 +745,6 @@ void deleteFold( | |||||||
|     buf_updates_send_changes(curbuf, first_lnum, num_changed, |     buf_updates_send_changes(curbuf, first_lnum, num_changed, | ||||||
|                              num_changed, true); |                              num_changed, true); | ||||||
|   } |   } | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /* clearFolding() {{{2 */ | /* clearFolding() {{{2 */ | ||||||
| @@ -1584,13 +1582,11 @@ static void foldCreateMarkers(linenr_T start, linenr_T end) | |||||||
|    * changed when the start marker is inserted and the end isn't. */ |    * changed when the start marker is inserted and the end isn't. */ | ||||||
|   changed_lines(start, (colnr_T)0, end, 0L, false); |   changed_lines(start, (colnr_T)0, end, 0L, false); | ||||||
|  |  | ||||||
|   if (kv_size(curbuf->update_channels)) { |  | ||||||
|   // Note: foldAddMarker() may not actually change start and/or end if |   // Note: foldAddMarker() may not actually change start and/or end if | ||||||
|   // u_save() is unable to save the buffer line, but we send the |   // u_save() is unable to save the buffer line, but we send the | ||||||
|   // nvim_buf_lines_event anyway since it won't do any harm. |   // nvim_buf_lines_event anyway since it won't do any harm. | ||||||
|   int64_t num_changed = 1 + end - start; |   int64_t num_changed = 1 + end - start; | ||||||
|   buf_updates_send_changes(curbuf, start, num_changed, num_changed, true); |   buf_updates_send_changes(curbuf, start, num_changed, num_changed, true); | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /* foldAddMarker() {{{2 */ | /* foldAddMarker() {{{2 */ | ||||||
|   | |||||||
| @@ -135,7 +135,7 @@ for i,f in ipairs(shallowcopy(functions)) do | |||||||
| end | end | ||||||
|  |  | ||||||
| -- don't expose internal attributes like "impl_name" in public metadata | -- don't expose internal attributes like "impl_name" in public metadata | ||||||
| exported_attributes = {'name', 'parameters', 'return_type', 'method', | exported_attributes = {'name', 'return_type', 'method', | ||||||
|                        'since', 'deprecated_since'} |                        'since', 'deprecated_since'} | ||||||
| exported_functions = {} | exported_functions = {} | ||||||
| for _,f in ipairs(functions) do | for _,f in ipairs(functions) do | ||||||
| @@ -144,6 +144,13 @@ for _,f in ipairs(functions) do | |||||||
|     for _,attr in ipairs(exported_attributes) do |     for _,attr in ipairs(exported_attributes) do | ||||||
|       f_exported[attr] = f[attr] |       f_exported[attr] = f[attr] | ||||||
|     end |     end | ||||||
|  |     f_exported.parameters = {} | ||||||
|  |     for i,param in ipairs(f.parameters) do | ||||||
|  |       if param[1] == "DictionaryOf(LuaRef)" then | ||||||
|  |         param = {"Dictionary", param[2]} | ||||||
|  |       end | ||||||
|  |       f_exported.parameters[i] = param | ||||||
|  |     end | ||||||
|     exported_functions[#exported_functions+1] = f_exported |     exported_functions[#exported_functions+1] = f_exported | ||||||
|   end |   end | ||||||
| end | end | ||||||
| @@ -371,14 +378,18 @@ local function process_function(fn) | |||||||
|     param = fn.parameters[j] |     param = fn.parameters[j] | ||||||
|     cparam = string.format('arg%u', j) |     cparam = string.format('arg%u', j) | ||||||
|     param_type = real_type(param[1]) |     param_type = real_type(param[1]) | ||||||
|     lc_param_type = param_type:lower() |     lc_param_type = real_type(param[1]):lower() | ||||||
|  |     extra = ((param_type == "Object" or param_type == "Dictionary") and "false, ") or "" | ||||||
|  |     if param[1] == "DictionaryOf(LuaRef)" then | ||||||
|  |       extra = "true, " | ||||||
|  |     end | ||||||
|     write_shifted_output(output, string.format([[ |     write_shifted_output(output, string.format([[ | ||||||
|     const %s %s = nlua_pop_%s(lstate, &err); |     const %s %s = nlua_pop_%s(lstate, %s&err); | ||||||
|  |  | ||||||
|     if (ERROR_SET(&err)) { |     if (ERROR_SET(&err)) { | ||||||
|       goto exit_%u; |       goto exit_%u; | ||||||
|     } |     } | ||||||
|     ]], param[1], cparam, param_type, #fn.parameters - j)) |     ]], param[1], cparam, param_type, extra, #fn.parameters - j)) | ||||||
|     free_code[#free_code + 1] = ('api_free_%s(%s);'):format( |     free_code[#free_code + 1] = ('api_free_%s(%s);'):format( | ||||||
|       lc_param_type, cparam) |       lc_param_type, cparam) | ||||||
|     cparams = cparam .. ', ' .. cparams |     cparams = cparam .. ', ' .. cparams | ||||||
|   | |||||||
| @@ -706,6 +706,10 @@ void nlua_push_Object(lua_State *lstate, const Object obj) | |||||||
|       lua_pushnil(lstate); |       lua_pushnil(lstate); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|  |     case kObjectTypeLuaRef: { | ||||||
|  |       nlua_pushref(lstate, obj.data.luaref); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
| #define ADD_TYPE(type, data_key) \ | #define ADD_TYPE(type, data_key) \ | ||||||
|     case kObjectType##type: { \ |     case kObjectType##type: { \ | ||||||
|       nlua_push_##type(lstate, obj.data.data_key); \ |       nlua_push_##type(lstate, obj.data.data_key); \ | ||||||
| @@ -862,7 +866,7 @@ static Array nlua_pop_Array_unchecked(lua_State *const lstate, | |||||||
|  |  | ||||||
|     lua_rawgeti(lstate, -1, (int)i); |     lua_rawgeti(lstate, -1, (int)i); | ||||||
|  |  | ||||||
|     val = nlua_pop_Object(lstate, err); |     val = nlua_pop_Object(lstate, false, err); | ||||||
|     if (ERROR_SET(err)) { |     if (ERROR_SET(err)) { | ||||||
|       ret.size = i - 1; |       ret.size = i - 1; | ||||||
|       lua_pop(lstate, 1); |       lua_pop(lstate, 1); | ||||||
| @@ -900,6 +904,7 @@ Array nlua_pop_Array(lua_State *lstate, Error *err) | |||||||
| /// @param[out]  err  Location where error will be saved. | /// @param[out]  err  Location where error will be saved. | ||||||
| static Dictionary nlua_pop_Dictionary_unchecked(lua_State *lstate, | static Dictionary nlua_pop_Dictionary_unchecked(lua_State *lstate, | ||||||
|                                                 const LuaTableProps table_props, |                                                 const LuaTableProps table_props, | ||||||
|  |                                                 bool ref, | ||||||
|                                                 Error *err) |                                                 Error *err) | ||||||
|   FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT |   FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT | ||||||
| { | { | ||||||
| @@ -923,7 +928,7 @@ static Dictionary nlua_pop_Dictionary_unchecked(lua_State *lstate, | |||||||
|       // stack: dict, key, value |       // stack: dict, key, value | ||||||
|  |  | ||||||
|       if (!ERROR_SET(err)) { |       if (!ERROR_SET(err)) { | ||||||
|         ret.items[i].value = nlua_pop_Object(lstate, err); |         ret.items[i].value = nlua_pop_Object(lstate, ref, err); | ||||||
|         // stack: dict, key |         // stack: dict, key | ||||||
|       } else { |       } else { | ||||||
|         lua_pop(lstate, 1); |         lua_pop(lstate, 1); | ||||||
| @@ -951,7 +956,7 @@ static Dictionary nlua_pop_Dictionary_unchecked(lua_State *lstate, | |||||||
| /// Convert lua table to dictionary | /// Convert lua table to dictionary | ||||||
| /// | /// | ||||||
| /// Always pops one value from the stack. | /// Always pops one value from the stack. | ||||||
| Dictionary nlua_pop_Dictionary(lua_State *lstate, Error *err) | Dictionary nlua_pop_Dictionary(lua_State *lstate, bool ref, Error *err) | ||||||
|   FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT |   FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT | ||||||
| { | { | ||||||
|   const LuaTableProps table_props = nlua_check_type(lstate, err, |   const LuaTableProps table_props = nlua_check_type(lstate, err, | ||||||
| @@ -961,7 +966,7 @@ Dictionary nlua_pop_Dictionary(lua_State *lstate, Error *err) | |||||||
|     return (Dictionary) { .size = 0, .items = NULL }; |     return (Dictionary) { .size = 0, .items = NULL }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return nlua_pop_Dictionary_unchecked(lstate, table_props, err); |   return nlua_pop_Dictionary_unchecked(lstate, table_props, ref, err); | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Helper structure for nlua_pop_Object | /// Helper structure for nlua_pop_Object | ||||||
| @@ -973,7 +978,7 @@ typedef struct { | |||||||
| /// Convert lua table to object | /// Convert lua table to object | ||||||
| /// | /// | ||||||
| /// Always pops one value from the stack. | /// Always pops one value from the stack. | ||||||
| Object nlua_pop_Object(lua_State *const lstate, Error *const err) | Object nlua_pop_Object(lua_State *const lstate, bool ref, Error *const err) | ||||||
| { | { | ||||||
|   Object ret = NIL; |   Object ret = NIL; | ||||||
|   const int initial_size = lua_gettop(lstate); |   const int initial_size = lua_gettop(lstate); | ||||||
| @@ -1122,7 +1127,18 @@ Object nlua_pop_Object(lua_State *const lstate, Error *const err) | |||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       case LUA_TFUNCTION: { | ||||||
|  |         if (ref) { | ||||||
|  |           *cur.obj = LUAREF_OBJ(nlua_ref(lstate, -1)); | ||||||
|  |         } else { | ||||||
|  |           goto type_error; | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       default: { |       default: { | ||||||
|  | type_error: | ||||||
|         api_set_error(err, kErrorTypeValidation, |         api_set_error(err, kErrorTypeValidation, | ||||||
|                       "Cannot convert given lua type"); |                       "Cannot convert given lua type"); | ||||||
|         break; |         break; | ||||||
|   | |||||||
| @@ -363,6 +363,33 @@ static int nlua_getenv(lua_State *lstate) | |||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | /// add the value to the registry | ||||||
|  | LuaRef nlua_ref(lua_State *lstate, int index) | ||||||
|  | { | ||||||
|  |   lua_pushvalue(lstate, index); | ||||||
|  |   return luaL_ref(lstate, LUA_REGISTRYINDEX); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// remove the value from the registry | ||||||
|  | void nlua_unref(lua_State *lstate, LuaRef ref) | ||||||
|  | { | ||||||
|  |   if (ref > 0) { | ||||||
|  |     luaL_unref(lstate, LUA_REGISTRYINDEX, ref); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void executor_free_luaref(LuaRef ref) | ||||||
|  | { | ||||||
|  |   lua_State *const lstate = nlua_enter(); | ||||||
|  |   nlua_unref(lstate, ref); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// push a value referenced in the regirstry | ||||||
|  | void nlua_pushref(lua_State *lstate, LuaRef ref) | ||||||
|  | { | ||||||
|  |   lua_rawgeti(lstate, LUA_REGISTRYINDEX, ref); | ||||||
|  | } | ||||||
|  |  | ||||||
| /// Evaluate lua string | /// Evaluate lua string | ||||||
| /// | /// | ||||||
| /// Used for luaeval(). | /// Used for luaeval(). | ||||||
| @@ -451,9 +478,29 @@ Object executor_exec_lua_api(const String str, const Array args, Error *err) | |||||||
|     return NIL; |     return NIL; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return nlua_pop_Object(lstate, err); |   return nlua_pop_Object(lstate, false, err); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | Object executor_exec_lua_cb(LuaRef ref, const char *name, Array args) | ||||||
|  | { | ||||||
|  |   lua_State *const lstate = nlua_enter(); | ||||||
|  |   nlua_pushref(lstate, ref); | ||||||
|  |   lua_pushstring(lstate, name); | ||||||
|  |   for (size_t i = 0; i < args.size; i++) { | ||||||
|  |     nlua_push_Object(lstate, args.items[i]); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (lua_pcall(lstate, (int)args.size+1, 1, 0)) { | ||||||
|  |     // TODO(bfredl): callbacks:s might not always be msg-safe, for instance | ||||||
|  |     // lua callbacks for redraw events. Later on let the caller deal with the | ||||||
|  |     // error instead. | ||||||
|  |     nlua_error(lstate, _("Error executing lua callback: %.*s")); | ||||||
|  |     return NIL; | ||||||
|  |   } | ||||||
|  |   Error err = ERROR_INIT; | ||||||
|  |  | ||||||
|  |   return nlua_pop_Object(lstate, false, &err); | ||||||
|  | } | ||||||
|  |  | ||||||
| /// Run lua string | /// Run lua string | ||||||
| /// | /// | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| #define NVIM_LUA_EXECUTOR_H | #define NVIM_LUA_EXECUTOR_H | ||||||
|  |  | ||||||
| #include <lua.h> | #include <lua.h> | ||||||
|  | #include <lauxlib.h> | ||||||
|  |  | ||||||
| #include "nvim/api/private/defs.h" | #include "nvim/api/private/defs.h" | ||||||
| #include "nvim/func_attr.h" | #include "nvim/func_attr.h" | ||||||
|   | |||||||
| @@ -1847,9 +1847,7 @@ void changed_bytes(linenr_T lnum, colnr_T col) | |||||||
|   changedOneline(curbuf, lnum); |   changedOneline(curbuf, lnum); | ||||||
|   changed_common(lnum, col, lnum + 1, 0L); |   changed_common(lnum, col, lnum + 1, 0L); | ||||||
|   // notify any channels that are watching |   // notify any channels that are watching | ||||||
|   if (kv_size(curbuf->update_channels)) { |  | ||||||
|   buf_updates_send_changes(curbuf, lnum, 1, 1, true); |   buf_updates_send_changes(curbuf, lnum, 1, 1, true); | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /* Diff highlighting in other diff windows may need to be updated too. */ |   /* Diff highlighting in other diff windows may need to be updated too. */ | ||||||
|   if (curwin->w_p_diff) { |   if (curwin->w_p_diff) { | ||||||
| @@ -1973,7 +1971,7 @@ changed_lines( | |||||||
|  |  | ||||||
|   changed_common(lnum, col, lnume, xtra); |   changed_common(lnum, col, lnume, xtra); | ||||||
|  |  | ||||||
|   if (do_buf_event && kv_size(curbuf->update_channels)) { |   if (do_buf_event) { | ||||||
|     int64_t num_added = (int64_t)(lnume + xtra - lnum); |     int64_t num_added = (int64_t)(lnume + xtra - lnum); | ||||||
|     int64_t num_removed = lnume - lnum; |     int64_t num_removed = lnume - lnum; | ||||||
|     buf_updates_send_changes(curbuf, lnum, num_added, num_removed, true); |     buf_updates_send_changes(curbuf, lnum, num_added, num_removed, true); | ||||||
|   | |||||||
| @@ -253,7 +253,8 @@ bool msgpack_rpc_to_object(const msgpack_object *const obj, Object *const arg) | |||||||
|           case kObjectTypeFloat: |           case kObjectTypeFloat: | ||||||
|           case kObjectTypeString: |           case kObjectTypeString: | ||||||
|           case kObjectTypeArray: |           case kObjectTypeArray: | ||||||
|           case kObjectTypeDictionary: { |           case kObjectTypeDictionary: | ||||||
|  |           case kObjectTypeLuaRef: { | ||||||
|             break; |             break; | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @@ -387,6 +388,13 @@ void msgpack_rpc_from_object(const Object result, msgpack_packer *const res) | |||||||
|         msgpack_pack_nil(res); |         msgpack_pack_nil(res); | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|  |       case kObjectTypeLuaRef: { | ||||||
|  |         // TODO(bfredl): could also be an error. Though kObjectTypeLuaRef | ||||||
|  |         // should only appear when the caller has opted in to handle references, | ||||||
|  |         // see nlua_pop_Object. | ||||||
|  |         msgpack_pack_nil(res); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|       case kObjectTypeBoolean: { |       case kObjectTypeBoolean: { | ||||||
|         msgpack_rpc_from_boolean(cur.aobj->data.boolean, res); |         msgpack_rpc_from_boolean(cur.aobj->data.boolean, res); | ||||||
|         break; |         break; | ||||||
|   | |||||||
| @@ -16,6 +16,11 @@ typedef uint32_t u8char_T; | |||||||
| // Opaque handle used by API clients to refer to various objects in vim | // Opaque handle used by API clients to refer to various objects in vim | ||||||
| typedef int handle_T; | typedef int handle_T; | ||||||
|  |  | ||||||
|  | // Opaque handle to a lua value. Must be free with `executor_free_luaref` when | ||||||
|  | // not needed anymore! LUA_NOREF represents missing reference, i e to indicate | ||||||
|  | // absent callback etc. | ||||||
|  | typedef int LuaRef; | ||||||
|  |  | ||||||
| typedef struct expand expand_T; | typedef struct expand expand_T; | ||||||
|  |  | ||||||
| #endif  // NVIM_TYPES_H | #endif  // NVIM_TYPES_H | ||||||
|   | |||||||
| @@ -2299,7 +2299,7 @@ static void u_undoredo(int undo, bool do_buf_event) | |||||||
|   // because the calls to changed()/unchanged() above will bump changedtick |   // because the calls to changed()/unchanged() above will bump changedtick | ||||||
|   // again, we need to send a nvim_buf_lines_event with just the new value of |   // again, we need to send a nvim_buf_lines_event with just the new value of | ||||||
|   // b:changedtick |   // b:changedtick | ||||||
|   if (do_buf_event && kv_size(curbuf->update_channels)) { |   if (do_buf_event) { | ||||||
|     buf_updates_changedtick(curbuf); |     buf_updates_changedtick(curbuf); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -760,7 +760,7 @@ describe('API: buffer events:', function() | |||||||
|   it('returns a proper error on nonempty options dict', function() |   it('returns a proper error on nonempty options dict', function() | ||||||
|     clear() |     clear() | ||||||
|     local b = editoriginal(false) |     local b = editoriginal(false) | ||||||
|     expect_err("dict isn't empty", buffer, 'attach', b, false, {builtin="asfd"}) |     expect_err("unexpected key: builtin", buffer, 'attach', b, false, {builtin="asfd"}) | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
|   it('nvim_buf_attach returns response after delay #8634', function() |   it('nvim_buf_attach returns response after delay #8634', function() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Björn Linse
					Björn Linse