mirror of
https://github.com/neovim/neovim.git
synced 2026-01-18 02:59:25 +10:00
When a buffer update callback is called, textlock is active so buffer text cannot be changed, but cursor can still be moved. This can cause problems when the buffer update is in the middle of an operator, like the one mentioned in #16729. The solution is to save cursor position and restore it afterwards, like how cursor is saved and restored when evaluating an <expr> mapping.
434 lines
13 KiB
C
434 lines
13 KiB
C
// This is an open source non-commercial project. Dear PVS-Studio, please check
|
|
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
|
|
|
|
#include <inttypes.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
|
|
#include "klib/kvec.h"
|
|
#include "lauxlib.h"
|
|
#include "nvim/api/buffer.h"
|
|
#include "nvim/api/private/defs.h"
|
|
#include "nvim/api/private/helpers.h"
|
|
#include "nvim/assert.h"
|
|
#include "nvim/buffer.h"
|
|
#include "nvim/buffer_defs.h"
|
|
#include "nvim/buffer_updates.h"
|
|
#include "nvim/extmark.h"
|
|
#include "nvim/globals.h"
|
|
#include "nvim/log.h"
|
|
#include "nvim/lua/executor.h"
|
|
#include "nvim/memline.h"
|
|
#include "nvim/memory.h"
|
|
#include "nvim/msgpack_rpc/channel.h"
|
|
#include "nvim/pos.h"
|
|
#include "nvim/types.h"
|
|
|
|
#ifdef INCLUDE_GENERATED_DECLARATIONS
|
|
# include "buffer_updates.c.generated.h" // IWYU pragma: export
|
|
#endif
|
|
|
|
// 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
|
|
// unloaded.
|
|
bool buf_updates_register(buf_T *buf, uint64_t channel_id, BufUpdateCallbacks cb, bool send_buffer)
|
|
{
|
|
// must fail if the buffer isn't loaded
|
|
if (buf->b_ml.ml_mfp == NULL) {
|
|
return false;
|
|
}
|
|
|
|
if (channel_id == LUA_INTERNAL_CALL) {
|
|
kv_push(buf->update_callbacks, cb);
|
|
if (cb.utf_sizes) {
|
|
buf->update_need_codepoints = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// count how many channels are currently watching the buffer
|
|
size_t size = kv_size(buf->update_channels);
|
|
for (size_t i = 0; i < size; i++) {
|
|
if (kv_A(buf->update_channels, i) == channel_id) {
|
|
// buffer is already registered ... nothing to do
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// append the channelid to the list
|
|
kv_push(buf->update_channels, channel_id);
|
|
|
|
if (send_buffer) {
|
|
Array args = ARRAY_DICT_INIT;
|
|
args.size = 6;
|
|
args.items = xcalloc(args.size, sizeof(Object));
|
|
|
|
// the first argument is always the buffer handle
|
|
args.items[0] = BUFFER_OBJ(buf->handle);
|
|
args.items[1] = INTEGER_OBJ(buf_get_changedtick(buf));
|
|
// the first line that changed (zero-indexed)
|
|
args.items[2] = INTEGER_OBJ(0);
|
|
// the last line that was changed
|
|
args.items[3] = INTEGER_OBJ(-1);
|
|
Array linedata = ARRAY_DICT_INIT;
|
|
|
|
// collect buffer contents
|
|
|
|
STATIC_ASSERT(SIZE_MAX >= MAXLNUM, "size_t smaller than MAXLNUM");
|
|
size_t line_count = (size_t)buf->b_ml.ml_line_count;
|
|
|
|
if (line_count >= 1) {
|
|
linedata.size = line_count;
|
|
linedata.items = xcalloc(line_count, sizeof(Object));
|
|
|
|
buf_collect_lines(buf, line_count, 1, 0, true, &linedata, NULL, NULL);
|
|
}
|
|
|
|
args.items[4] = ARRAY_OBJ(linedata);
|
|
args.items[5] = BOOLEAN_OBJ(false);
|
|
|
|
rpc_send_event(channel_id, "nvim_buf_lines_event", args);
|
|
api_free_array(args); // TODO(bfredl): no
|
|
} else {
|
|
buf_updates_changedtick_single(buf, channel_id);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool buf_updates_active(buf_T *buf)
|
|
FUNC_ATTR_PURE
|
|
{
|
|
return kv_size(buf->update_channels) || kv_size(buf->update_callbacks);
|
|
}
|
|
|
|
void buf_updates_send_end(buf_T *buf, uint64_t channelid)
|
|
{
|
|
MAXSIZE_TEMP_ARRAY(args, 1);
|
|
ADD_C(args, BUFFER_OBJ(buf->handle));
|
|
rpc_send_event(channelid, "nvim_buf_detach_event", args);
|
|
}
|
|
|
|
void buf_updates_unregister(buf_T *buf, uint64_t channelid)
|
|
{
|
|
size_t size = kv_size(buf->update_channels);
|
|
if (!size) {
|
|
return;
|
|
}
|
|
|
|
// go through list backwards and remove the channel id each time it appears
|
|
// (it should never appear more than once)
|
|
size_t j = 0;
|
|
size_t found = 0;
|
|
for (size_t i = 0; i < size; i++) {
|
|
if (kv_A(buf->update_channels, i) == channelid) {
|
|
found++;
|
|
} else {
|
|
// copy item backwards into prior slot if needed
|
|
if (i != j) {
|
|
kv_A(buf->update_channels, j) = kv_A(buf->update_channels, i);
|
|
}
|
|
j++;
|
|
}
|
|
}
|
|
|
|
if (found) {
|
|
// remove X items from the end of the array
|
|
buf->update_channels.size -= found;
|
|
|
|
// make a new copy of the active array without the channelid in it
|
|
buf_updates_send_end(buf, channelid);
|
|
|
|
if (found == size) {
|
|
kv_destroy(buf->update_channels);
|
|
kv_init(buf->update_channels);
|
|
}
|
|
}
|
|
}
|
|
|
|
void buf_free_callbacks(buf_T *buf)
|
|
{
|
|
kv_destroy(buf->update_channels);
|
|
for (size_t i = 0; i < kv_size(buf->update_callbacks); i++) {
|
|
buffer_update_callbacks_free(kv_A(buf->update_callbacks, i));
|
|
}
|
|
kv_destroy(buf->update_callbacks);
|
|
}
|
|
|
|
void buf_updates_unload(buf_T *buf, bool can_reload)
|
|
{
|
|
size_t size = kv_size(buf->update_channels);
|
|
if (size) {
|
|
for (size_t i = 0; i < size; i++) {
|
|
buf_updates_send_end(buf, kv_A(buf->update_channels, i));
|
|
}
|
|
kv_destroy(buf->update_channels);
|
|
kv_init(buf->update_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);
|
|
LuaRef thecb = LUA_NOREF;
|
|
|
|
bool keep = false;
|
|
if (can_reload && cb.on_reload != LUA_NOREF) {
|
|
keep = true;
|
|
thecb = cb.on_reload;
|
|
} else if (cb.on_detach != LUA_NOREF) {
|
|
thecb = cb.on_detach;
|
|
}
|
|
|
|
if (thecb != LUA_NOREF) {
|
|
Array args = ARRAY_DICT_INIT;
|
|
Object items[1];
|
|
args.size = 1;
|
|
args.items = items;
|
|
|
|
// the first argument is always the buffer handle
|
|
args.items[0] = BUFFER_OBJ(buf->handle);
|
|
|
|
TEXTLOCK_WRAP({
|
|
nlua_call_ref(thecb, keep ? "reload" : "detach", args, false, NULL);
|
|
});
|
|
}
|
|
|
|
if (keep) {
|
|
kv_A(buf->update_callbacks, j++) = kv_A(buf->update_callbacks, i);
|
|
} else {
|
|
buffer_update_callbacks_free(cb);
|
|
}
|
|
}
|
|
kv_size(buf->update_callbacks) = j;
|
|
if (kv_size(buf->update_callbacks) == 0) {
|
|
kv_destroy(buf->update_callbacks);
|
|
kv_init(buf->update_callbacks);
|
|
}
|
|
}
|
|
|
|
void buf_updates_send_changes(buf_T *buf, linenr_T firstline, int64_t num_added,
|
|
int64_t num_removed)
|
|
{
|
|
size_t deleted_codepoints, deleted_codeunits;
|
|
size_t deleted_bytes = ml_flush_deleted_bytes(buf, &deleted_codepoints,
|
|
&deleted_codeunits);
|
|
|
|
if (!buf_updates_active(buf)) {
|
|
return;
|
|
}
|
|
|
|
// Don't send b:changedtick during 'inccommand' preview if "buf" is the current buffer.
|
|
bool send_tick = !(cmdpreview && buf == curbuf);
|
|
|
|
// if one the channels doesn't work, put its ID here so we can remove it later
|
|
uint64_t badchannelid = 0;
|
|
|
|
// notify each of the active channels
|
|
for (size_t i = 0; i < kv_size(buf->update_channels); i++) {
|
|
uint64_t channelid = kv_A(buf->update_channels, i);
|
|
|
|
// send through the changes now channel contents now
|
|
Array args = ARRAY_DICT_INIT;
|
|
args.size = 6;
|
|
args.items = xcalloc(args.size, sizeof(Object));
|
|
|
|
// 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);
|
|
|
|
// linedata of lines being swapped in
|
|
Array linedata = ARRAY_DICT_INIT;
|
|
if (num_added > 0) {
|
|
STATIC_ASSERT(SIZE_MAX >= MAXLNUM, "size_t smaller than MAXLNUM");
|
|
linedata.size = (size_t)num_added;
|
|
linedata.items = xcalloc((size_t)num_added, sizeof(Object));
|
|
buf_collect_lines(buf, (size_t)num_added, firstline, 0, true, &linedata,
|
|
NULL, NULL);
|
|
}
|
|
args.items[4] = ARRAY_OBJ(linedata);
|
|
args.items[5] = BOOLEAN_OBJ(false);
|
|
if (!rpc_send_event(channelid, "nvim_buf_lines_event", args)) {
|
|
// We can't unregister the channel while we're iterating over the
|
|
// update_channels array, so we remember its ID to unregister it at
|
|
// the end.
|
|
badchannelid = channelid;
|
|
}
|
|
api_free_array(args); // TODO(bfredl): no
|
|
}
|
|
|
|
// We can only ever remove one dead channel at a time. This is OK because the
|
|
// change notifications are so frequent that many dead channels will be
|
|
// cleared up quickly.
|
|
if (badchannelid != 0) {
|
|
ELOG("Disabling buffer updates for dead channel %" PRIu64, 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 && (cb.preview || !cmdpreview)) {
|
|
Array args = ARRAY_DICT_INIT;
|
|
Object items[8];
|
|
args.size = 6; // may be increased to 8 below
|
|
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);
|
|
|
|
// byte count of previous contents
|
|
args.items[5] = INTEGER_OBJ((Integer)deleted_bytes);
|
|
if (cb.utf_sizes) {
|
|
args.size = 8;
|
|
args.items[6] = INTEGER_OBJ((Integer)deleted_codepoints);
|
|
args.items[7] = INTEGER_OBJ((Integer)deleted_codeunits);
|
|
}
|
|
|
|
Object res;
|
|
TEXTLOCK_WRAP({
|
|
res = nlua_call_ref(cb.on_lines, "lines", args, false, NULL);
|
|
});
|
|
|
|
if (res.type == kObjectTypeBoolean && res.data.boolean == true) {
|
|
buffer_update_callbacks_free(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_send_splice(buf_T *buf, int start_row, colnr_T start_col, bcount_t start_byte,
|
|
int old_row, colnr_T old_col, bcount_t old_byte, int new_row,
|
|
colnr_T new_col, bcount_t new_byte)
|
|
{
|
|
if (!buf_updates_active(buf)
|
|
|| (old_byte == 0 && new_byte == 0)) {
|
|
return;
|
|
}
|
|
|
|
// notify each of the active callbacks
|
|
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_bytes != LUA_NOREF && (cb.preview || !cmdpreview)) {
|
|
MAXSIZE_TEMP_ARRAY(args, 11);
|
|
|
|
// the first argument is always the buffer handle
|
|
ADD_C(args, BUFFER_OBJ(buf->handle));
|
|
|
|
// next argument is b:changedtick
|
|
ADD_C(args, INTEGER_OBJ(buf_get_changedtick(buf)));
|
|
|
|
ADD_C(args, INTEGER_OBJ(start_row));
|
|
ADD_C(args, INTEGER_OBJ(start_col));
|
|
ADD_C(args, INTEGER_OBJ(start_byte));
|
|
ADD_C(args, INTEGER_OBJ(old_row));
|
|
ADD_C(args, INTEGER_OBJ(old_col));
|
|
ADD_C(args, INTEGER_OBJ(old_byte));
|
|
ADD_C(args, INTEGER_OBJ(new_row));
|
|
ADD_C(args, INTEGER_OBJ(new_col));
|
|
ADD_C(args, INTEGER_OBJ(new_byte));
|
|
|
|
Object res;
|
|
TEXTLOCK_WRAP({
|
|
res = nlua_call_ref(cb.on_bytes, "bytes", args, false, NULL);
|
|
});
|
|
|
|
if (res.type == kObjectTypeBoolean && res.data.boolean == true) {
|
|
buffer_update_callbacks_free(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)
|
|
{
|
|
// notify each of the active channels
|
|
for (size_t i = 0; i < kv_size(buf->update_channels); i++) {
|
|
uint64_t channel_id = kv_A(buf->update_channels, i);
|
|
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) {
|
|
MAXSIZE_TEMP_ARRAY(args, 2);
|
|
|
|
// the first argument is always the buffer handle
|
|
ADD_C(args, BUFFER_OBJ(buf->handle));
|
|
|
|
// next argument is b:changedtick
|
|
ADD_C(args, INTEGER_OBJ(buf_get_changedtick(buf)));
|
|
|
|
Object res;
|
|
TEXTLOCK_WRAP({
|
|
res = nlua_call_ref(cb.on_changedtick, "changedtick", args, false, NULL);
|
|
});
|
|
|
|
if (res.type == kObjectTypeBoolean && res.data.boolean == true) {
|
|
buffer_update_callbacks_free(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)
|
|
{
|
|
MAXSIZE_TEMP_ARRAY(args, 2);
|
|
|
|
// the first argument is always the buffer handle
|
|
ADD_C(args, BUFFER_OBJ(buf->handle));
|
|
|
|
// next argument is b:changedtick
|
|
ADD_C(args, INTEGER_OBJ(buf_get_changedtick(buf)));
|
|
|
|
// don't try and clean up dead channels here
|
|
rpc_send_event(channel_id, "nvim_buf_changedtick_event", args);
|
|
}
|
|
|
|
void buffer_update_callbacks_free(BufUpdateCallbacks cb)
|
|
{
|
|
api_free_luaref(cb.on_lines);
|
|
api_free_luaref(cb.on_bytes);
|
|
api_free_luaref(cb.on_changedtick);
|
|
api_free_luaref(cb.on_reload);
|
|
api_free_luaref(cb.on_detach);
|
|
}
|