Files
neovim/src/nvim/buffer_updates.c
Famiu Haque 46536f53e8 feat: add preview functionality to user commands
Adds a Lua-only `preview` flag to user commands which allows the command to be incrementally previewed like `:substitute` when 'inccommand' is set.
2022-05-31 20:55:05 +06:00

414 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 "nvim/api/private/helpers.h"
#include "nvim/assert.h"
#include "nvim/buffer.h"
#include "nvim/buffer_updates.h"
#include "nvim/extmark.h"
#include "nvim/lua/executor.h"
#include "nvim/memline.h"
#include "nvim/msgpack_rpc/channel.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.
// 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);
if (size) {
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, true, &linedata, NULL);
}
args.items[4] = ARRAY_OBJ(linedata);
args.items[5] = BOOLEAN_OBJ(false);
rpc_send_event(channel_id, "nvim_buf_lines_event", args);
} 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)
{
Array args = ARRAY_DICT_INIT;
args.size = 1;
args.items = xcalloc(args.size, sizeof(Object));
args.items[0] = 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_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++;
nlua_call_ref(thecb, keep ? "reload" : "detach", args, false, NULL);
textlock--;
}
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, true, &linedata,
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;
}
}
// 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);
}
textlock++;
Object res = nlua_call_ref(cb.on_lines, "lines", args, true, NULL);
textlock--;
if (res.type == kObjectTypeBoolean && res.data.boolean == true) {
buffer_update_callbacks_free(cb);
keep = false;
}
api_free_object(res);
}
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)) {
FIXED_TEMP_ARRAY(args, 11);
// 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));
args.items[2] = INTEGER_OBJ(start_row);
args.items[3] = INTEGER_OBJ(start_col);
args.items[4] = INTEGER_OBJ(start_byte);
args.items[5] = INTEGER_OBJ(old_row);
args.items[6] = INTEGER_OBJ(old_col);
args.items[7] = INTEGER_OBJ(old_byte);
args.items[8] = INTEGER_OBJ(new_row);
args.items[9] = INTEGER_OBJ(new_col);
args.items[10] = INTEGER_OBJ(new_byte);
textlock++;
Object res = nlua_call_ref(cb.on_bytes, "bytes", args, true, NULL);
textlock--;
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) {
FIXED_TEMP_ARRAY(args, 2);
// 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 = nlua_call_ref(cb.on_changedtick, "changedtick",
args, true, NULL);
textlock--;
if (res.type == kObjectTypeBoolean && res.data.boolean == true) {
buffer_update_callbacks_free(cb);
keep = false;
}
api_free_object(res);
}
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)
{
Array args = ARRAY_DICT_INIT;
args.size = 2;
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] = 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);
}