1// This is an open source non-commercial project. Dear PVS-Studio, please check
2// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3
4#include "nvim/buffer_updates.h"
5#include "nvim/memline.h"
6#include "nvim/api/private/helpers.h"
7#include "nvim/msgpack_rpc/channel.h"
8#include "nvim/lua/executor.h"
9#include "nvim/assert.h"
10#include "nvim/buffer.h"
11
12#ifdef INCLUDE_GENERATED_DECLARATIONS
13# include "buffer_updates.c.generated.h"
14#endif
15
16// Register a channel. Return True if the channel was added, or already added.
17// Return False if the channel couldn't be added because the buffer is
18// unloaded.
19bool buf_updates_register(buf_T *buf, uint64_t channel_id,
20 BufUpdateCallbacks cb, bool send_buffer)
21{
22 // must fail if the buffer isn't loaded
23 if (buf->b_ml.ml_mfp == NULL) {
24 return false;
25 }
26
27 if (channel_id == LUA_INTERNAL_CALL) {
28 kv_push(buf->update_callbacks, cb);
29 if (cb.utf_sizes) {
30 buf->update_need_codepoints = true;
31 }
32 return true;
33 }
34
35 // count how many channels are currently watching the buffer
36 size_t size = kv_size(buf->update_channels);
37 if (size) {
38 for (size_t i = 0; i < size; i++) {
39 if (kv_A(buf->update_channels, i) == channel_id) {
40 // buffer is already registered ... nothing to do
41 return true;
42 }
43 }
44 }
45
46 // append the channelid to the list
47 kv_push(buf->update_channels, channel_id);
48
49 if (send_buffer) {
50 Array args = ARRAY_DICT_INIT;
51 args.size = 6;
52 args.items = xcalloc(sizeof(Object), args.size);
53
54 // the first argument is always the buffer handle
55 args.items[0] = BUFFER_OBJ(buf->handle);
56 args.items[1] = INTEGER_OBJ(buf_get_changedtick(buf));
57 // the first line that changed (zero-indexed)
58 args.items[2] = INTEGER_OBJ(0);
59 // the last line that was changed
60 args.items[3] = INTEGER_OBJ(-1);
61 Array linedata = ARRAY_DICT_INIT;
62
63 // collect buffer contents
64
65 STATIC_ASSERT(SIZE_MAX >= MAXLNUM, "size_t smaller than MAXLNUM");
66 size_t line_count = (size_t)buf->b_ml.ml_line_count;
67
68 if (line_count >= 1) {
69 linedata.size = line_count;
70 linedata.items = xcalloc(sizeof(Object), line_count);
71
72 buf_collect_lines(buf, line_count, 1, true, &linedata, NULL);
73 }
74
75 args.items[4] = ARRAY_OBJ(linedata);
76 args.items[5] = BOOLEAN_OBJ(false);
77
78 rpc_send_event(channel_id, "nvim_buf_lines_event", args);
79 } else {
80 buf_updates_changedtick_single(buf, channel_id);
81 }
82
83 return true;
84}
85
86bool buf_updates_active(buf_T *buf)
87{
88 return kv_size(buf->update_channels) || kv_size(buf->update_callbacks);
89}
90
91void buf_updates_send_end(buf_T *buf, uint64_t channelid)
92{
93 Array args = ARRAY_DICT_INIT;
94 args.size = 1;
95 args.items = xcalloc(sizeof(Object), args.size);
96 args.items[0] = BUFFER_OBJ(buf->handle);
97 rpc_send_event(channelid, "nvim_buf_detach_event", args);
98}
99
100void buf_updates_unregister(buf_T *buf, uint64_t channelid)
101{
102 size_t size = kv_size(buf->update_channels);
103 if (!size) {
104 return;
105 }
106
107 // go through list backwards and remove the channel id each time it appears
108 // (it should never appear more than once)
109 size_t j = 0;
110 size_t found = 0;
111 for (size_t i = 0; i < size; i++) {
112 if (kv_A(buf->update_channels, i) == channelid) {
113 found++;
114 } else {
115 // copy item backwards into prior slot if needed
116 if (i != j) {
117 kv_A(buf->update_channels, j) = kv_A(buf->update_channels, i);
118 }
119 j++;
120 }
121 }
122
123 if (found) {
124 // remove X items from the end of the array
125 buf->update_channels.size -= found;
126
127 // make a new copy of the active array without the channelid in it
128 buf_updates_send_end(buf, channelid);
129
130 if (found == size) {
131 kv_destroy(buf->update_channels);
132 kv_init(buf->update_channels);
133 }
134 }
135}
136
137void buf_updates_unregister_all(buf_T *buf)
138{
139 size_t size = kv_size(buf->update_channels);
140 if (size) {
141 for (size_t i = 0; i < size; i++) {
142 buf_updates_send_end(buf, kv_A(buf->update_channels, i));
143 }
144 kv_destroy(buf->update_channels);
145 kv_init(buf->update_channels);
146 }
147
148 for (size_t i = 0; i < kv_size(buf->update_callbacks); i++) {
149 BufUpdateCallbacks cb = kv_A(buf->update_callbacks, i);
150 if (cb.on_detach != LUA_NOREF) {
151 Array args = ARRAY_DICT_INIT;
152 Object items[1];
153 args.size = 1;
154 args.items = items;
155
156 // the first argument is always the buffer handle
157 args.items[0] = BUFFER_OBJ(buf->handle);
158
159 textlock++;
160 executor_exec_lua_cb(cb.on_detach, "detach", args, false);
161 textlock--;
162 }
163 free_update_callbacks(cb);
164 }
165 kv_destroy(buf->update_callbacks);
166 kv_init(buf->update_callbacks);
167}
168
169void buf_updates_send_changes(buf_T *buf,
170 linenr_T firstline,
171 int64_t num_added,
172 int64_t num_removed,
173 bool send_tick)
174{
175 size_t deleted_codepoints, deleted_codeunits;
176 size_t deleted_bytes = ml_flush_deleted_bytes(buf, &deleted_codepoints,
177 &deleted_codeunits);
178
179 if (!buf_updates_active(buf)) {
180 return;
181 }
182
183 // if one the channels doesn't work, put its ID here so we can remove it later
184 uint64_t badchannelid = 0;
185
186 // notify each of the active channels
187 for (size_t i = 0; i < kv_size(buf->update_channels); i++) {
188 uint64_t channelid = kv_A(buf->update_channels, i);
189
190 // send through the changes now channel contents now
191 Array args = ARRAY_DICT_INIT;
192 args.size = 6;
193 args.items = xcalloc(sizeof(Object), args.size);
194
195 // the first argument is always the buffer handle
196 args.items[0] = BUFFER_OBJ(buf->handle);
197
198 // next argument is b:changedtick
199 args.items[1] = send_tick ? INTEGER_OBJ(buf_get_changedtick(buf)) : NIL;
200
201 // the first line that changed (zero-indexed)
202 args.items[2] = INTEGER_OBJ(firstline - 1);
203
204 // the last line that was changed
205 args.items[3] = INTEGER_OBJ(firstline - 1 + num_removed);
206
207 // linedata of lines being swapped in
208 Array linedata = ARRAY_DICT_INIT;
209 if (num_added > 0) {
210 STATIC_ASSERT(SIZE_MAX >= MAXLNUM, "size_t smaller than MAXLNUM");
211 linedata.size = (size_t)num_added;
212 linedata.items = xcalloc(sizeof(Object), (size_t)num_added);
213 buf_collect_lines(buf, (size_t)num_added, firstline, true, &linedata,
214 NULL);
215 }
216 args.items[4] = ARRAY_OBJ(linedata);
217 args.items[5] = BOOLEAN_OBJ(false);
218 if (!rpc_send_event(channelid, "nvim_buf_lines_event", args)) {
219 // We can't unregister the channel while we're iterating over the
220 // update_channels array, so we remember its ID to unregister it at
221 // the end.
222 badchannelid = channelid;
223 }
224 }
225
226 // We can only ever remove one dead channel at a time. This is OK because the
227 // change notifications are so frequent that many dead channels will be
228 // cleared up quickly.
229 if (badchannelid != 0) {
230 ELOG("Disabling buffer updates for dead channel %"PRIu64, badchannelid);
231 buf_updates_unregister(buf, badchannelid);
232 }
233
234 // notify each of the active channels
235 size_t j = 0;
236 for (size_t i = 0; i < kv_size(buf->update_callbacks); i++) {
237 BufUpdateCallbacks cb = kv_A(buf->update_callbacks, i);
238 bool keep = true;
239 if (cb.on_lines != LUA_NOREF) {
240 Array args = ARRAY_DICT_INIT;
241 Object items[8];
242 args.size = 6; // may be increased to 8 below
243 args.items = items;
244
245 // the first argument is always the buffer handle
246 args.items[0] = BUFFER_OBJ(buf->handle);
247
248 // next argument is b:changedtick
249 args.items[1] = send_tick ? INTEGER_OBJ(buf_get_changedtick(buf)) : NIL;
250
251 // the first line that changed (zero-indexed)
252 args.items[2] = INTEGER_OBJ(firstline - 1);
253
254 // the last line that was changed
255 args.items[3] = INTEGER_OBJ(firstline - 1 + num_removed);
256
257 // the last line in the updated range
258 args.items[4] = INTEGER_OBJ(firstline - 1 + num_added);
259
260 // byte count of previous contents
261 args.items[5] = INTEGER_OBJ((Integer)deleted_bytes);
262 if (cb.utf_sizes) {
263 args.size = 8;
264 args.items[6] = INTEGER_OBJ((Integer)deleted_codepoints);
265 args.items[7] = INTEGER_OBJ((Integer)deleted_codeunits);
266 }
267 textlock++;
268 Object res = executor_exec_lua_cb(cb.on_lines, "lines", args, true);
269 textlock--;
270
271 if (res.type == kObjectTypeBoolean && res.data.boolean == true) {
272 free_update_callbacks(cb);
273 keep = false;
274 }
275 api_free_object(res);
276 }
277 if (keep) {
278 kv_A(buf->update_callbacks, j++) = kv_A(buf->update_callbacks, i);
279 }
280 }
281 kv_size(buf->update_callbacks) = j;
282}
283
284void buf_updates_changedtick(buf_T *buf)
285{
286 // notify each of the active channels
287 for (size_t i = 0; i < kv_size(buf->update_channels); i++) {
288 uint64_t channel_id = kv_A(buf->update_channels, i);
289 buf_updates_changedtick_single(buf, channel_id);
290 }
291 size_t j = 0;
292 for (size_t i = 0; i < kv_size(buf->update_callbacks); i++) {
293 BufUpdateCallbacks cb = kv_A(buf->update_callbacks, i);
294 bool keep = true;
295 if (cb.on_changedtick != LUA_NOREF) {
296 Array args = ARRAY_DICT_INIT;
297 Object items[2];
298 args.size = 2;
299 args.items = items;
300
301 // the first argument is always the buffer handle
302 args.items[0] = BUFFER_OBJ(buf->handle);
303
304 // next argument is b:changedtick
305 args.items[1] = INTEGER_OBJ(buf_get_changedtick(buf));
306
307 textlock++;
308 Object res = executor_exec_lua_cb(cb.on_changedtick, "changedtick",
309 args, true);
310 textlock--;
311
312 if (res.type == kObjectTypeBoolean && res.data.boolean == true) {
313 free_update_callbacks(cb);
314 keep = false;
315 }
316 api_free_object(res);
317 }
318 if (keep) {
319 kv_A(buf->update_callbacks, j++) = kv_A(buf->update_callbacks, i);
320 }
321 }
322 kv_size(buf->update_callbacks) = j;
323}
324
325void buf_updates_changedtick_single(buf_T *buf, uint64_t channel_id)
326{
327 Array args = ARRAY_DICT_INIT;
328 args.size = 2;
329 args.items = xcalloc(sizeof(Object), args.size);
330
331 // the first argument is always the buffer handle
332 args.items[0] = BUFFER_OBJ(buf->handle);
333
334 // next argument is b:changedtick
335 args.items[1] = INTEGER_OBJ(buf_get_changedtick(buf));
336
337 // don't try and clean up dead channels here
338 rpc_send_event(channel_id, "nvim_buf_changedtick_event", args);
339}
340
341static void free_update_callbacks(BufUpdateCallbacks cb)
342{
343 executor_free_luaref(cb.on_lines);
344 executor_free_luaref(cb.on_changedtick);
345}
346