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 <assert.h> |
5 | #include <stddef.h> |
6 | #include <stdint.h> |
7 | #include <stdbool.h> |
8 | |
9 | #include "nvim/vim.h" |
10 | #include "nvim/ui.h" |
11 | #include "nvim/memory.h" |
12 | #include "nvim/map.h" |
13 | #include "nvim/msgpack_rpc/channel.h" |
14 | #include "nvim/api/ui.h" |
15 | #include "nvim/api/private/defs.h" |
16 | #include "nvim/api/private/helpers.h" |
17 | #include "nvim/popupmnu.h" |
18 | #include "nvim/cursor_shape.h" |
19 | #include "nvim/highlight.h" |
20 | #include "nvim/screen.h" |
21 | #include "nvim/window.h" |
22 | |
23 | #ifdef INCLUDE_GENERATED_DECLARATIONS |
24 | # include "api/ui.c.generated.h" |
25 | # include "ui_events_remote.generated.h" |
26 | #endif |
27 | |
28 | typedef struct { |
29 | uint64_t channel_id; |
30 | Array buffer; |
31 | |
32 | int hl_id; // Current highlight for legacy put event. |
33 | Integer cursor_row, cursor_col; // Intended visible cursor position. |
34 | |
35 | // Position of legacy cursor, used both for drawing and visible user cursor. |
36 | Integer client_row, client_col; |
37 | bool ; |
38 | } UIData; |
39 | |
40 | static PMap(uint64_t) *connected_uis = NULL; |
41 | |
42 | void remote_ui_init(void) |
43 | FUNC_API_NOEXPORT |
44 | { |
45 | connected_uis = pmap_new(uint64_t)(); |
46 | } |
47 | |
48 | void remote_ui_disconnect(uint64_t channel_id) |
49 | FUNC_API_NOEXPORT |
50 | { |
51 | UI *ui = pmap_get(uint64_t)(connected_uis, channel_id); |
52 | if (!ui) { |
53 | return; |
54 | } |
55 | UIData *data = ui->data; |
56 | api_free_array(data->buffer); // Destroy pending screen updates. |
57 | pmap_del(uint64_t)(connected_uis, channel_id); |
58 | xfree(ui->data); |
59 | ui->data = NULL; // Flag UI as "stopped". |
60 | ui_detach_impl(ui, channel_id); |
61 | xfree(ui); |
62 | } |
63 | |
64 | /// Wait until ui has connected on stdio channel. |
65 | void remote_ui_wait_for_attach(void) |
66 | FUNC_API_NOEXPORT |
67 | { |
68 | Channel *channel = find_channel(CHAN_STDIO); |
69 | if (!channel) { |
70 | // this function should only be called in --embed mode, stdio channel |
71 | // can be assumed. |
72 | abort(); |
73 | } |
74 | |
75 | LOOP_PROCESS_EVENTS_UNTIL(&main_loop, channel->events, -1, |
76 | pmap_has(uint64_t)(connected_uis, CHAN_STDIO)); |
77 | } |
78 | |
79 | /// Activates UI events on the channel. |
80 | /// |
81 | /// Entry point of all UI clients. Allows |\-\-embed| to continue startup. |
82 | /// Implies that the client is ready to show the UI. Adds the client to the |
83 | /// list of UIs. |nvim_list_uis()| |
84 | /// |
85 | /// @note If multiple UI clients are attached, the global screen dimensions |
86 | /// degrade to the smallest client. E.g. if client A requests 80x40 but |
87 | /// client B requests 200x100, the global screen has size 80x40. |
88 | /// |
89 | /// @param channel_id |
90 | /// @param width Requested screen columns |
91 | /// @param height Requested screen rows |
92 | /// @param options |ui-option| map |
93 | /// @param[out] err Error details, if any |
94 | void nvim_ui_attach(uint64_t channel_id, Integer width, Integer height, |
95 | Dictionary options, Error *err) |
96 | FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY |
97 | { |
98 | if (pmap_has(uint64_t)(connected_uis, channel_id)) { |
99 | api_set_error(err, kErrorTypeException, |
100 | "UI already attached to channel: %" PRId64, channel_id); |
101 | return; |
102 | } |
103 | |
104 | if (width <= 0 || height <= 0) { |
105 | api_set_error(err, kErrorTypeValidation, |
106 | "Expected width > 0 and height > 0" ); |
107 | return; |
108 | } |
109 | UI *ui = xcalloc(1, sizeof(UI)); |
110 | ui->width = (int)width; |
111 | ui->height = (int)height; |
112 | ui->pum_height = 0; |
113 | ui->rgb = true; |
114 | ui->override = false; |
115 | ui->grid_resize = remote_ui_grid_resize; |
116 | ui->grid_clear = remote_ui_grid_clear; |
117 | ui->grid_cursor_goto = remote_ui_grid_cursor_goto; |
118 | ui->mode_info_set = remote_ui_mode_info_set; |
119 | ui->update_menu = remote_ui_update_menu; |
120 | ui->busy_start = remote_ui_busy_start; |
121 | ui->busy_stop = remote_ui_busy_stop; |
122 | ui->mouse_on = remote_ui_mouse_on; |
123 | ui->mouse_off = remote_ui_mouse_off; |
124 | ui->mode_change = remote_ui_mode_change; |
125 | ui->grid_scroll = remote_ui_grid_scroll; |
126 | ui->hl_attr_define = remote_ui_hl_attr_define; |
127 | ui->hl_group_set = remote_ui_hl_group_set; |
128 | ui->raw_line = remote_ui_raw_line; |
129 | ui->bell = remote_ui_bell; |
130 | ui->visual_bell = remote_ui_visual_bell; |
131 | ui->default_colors_set = remote_ui_default_colors_set; |
132 | ui->flush = remote_ui_flush; |
133 | ui->suspend = remote_ui_suspend; |
134 | ui->set_title = remote_ui_set_title; |
135 | ui->set_icon = remote_ui_set_icon; |
136 | ui->option_set = remote_ui_option_set; |
137 | ui->msg_set_pos = remote_ui_msg_set_pos; |
138 | ui->event = remote_ui_event; |
139 | ui->inspect = remote_ui_inspect; |
140 | |
141 | memset(ui->ui_ext, 0, sizeof(ui->ui_ext)); |
142 | |
143 | for (size_t i = 0; i < options.size; i++) { |
144 | ui_set_option(ui, true, options.items[i].key, options.items[i].value, err); |
145 | if (ERROR_SET(err)) { |
146 | xfree(ui); |
147 | return; |
148 | } |
149 | } |
150 | |
151 | if (ui->ui_ext[kUIHlState] || ui->ui_ext[kUIMultigrid]) { |
152 | ui->ui_ext[kUILinegrid] = true; |
153 | } |
154 | |
155 | if (ui->ui_ext[kUIMessages]) { |
156 | // This uses attribute indicies, so ext_linegrid is needed. |
157 | ui->ui_ext[kUILinegrid] = true; |
158 | // Cmdline uses the messages area, so it should be externalized too. |
159 | ui->ui_ext[kUICmdline] = true; |
160 | } |
161 | |
162 | UIData *data = xmalloc(sizeof(UIData)); |
163 | data->channel_id = channel_id; |
164 | data->buffer = (Array)ARRAY_DICT_INIT; |
165 | data->hl_id = 0; |
166 | data->client_col = -1; |
167 | data->wildmenu_active = false; |
168 | ui->data = data; |
169 | |
170 | pmap_put(uint64_t)(connected_uis, channel_id, ui); |
171 | ui_attach_impl(ui, channel_id); |
172 | } |
173 | |
174 | /// @deprecated |
175 | void ui_attach(uint64_t channel_id, Integer width, Integer height, |
176 | Boolean enable_rgb, Error *err) |
177 | { |
178 | Dictionary opts = ARRAY_DICT_INIT; |
179 | PUT(opts, "rgb" , BOOLEAN_OBJ(enable_rgb)); |
180 | nvim_ui_attach(channel_id, width, height, opts, err); |
181 | api_free_dictionary(opts); |
182 | } |
183 | |
184 | /// Deactivates UI events on the channel. |
185 | /// |
186 | /// Removes the client from the list of UIs. |nvim_list_uis()| |
187 | /// |
188 | /// @param channel_id |
189 | /// @param[out] err Error details, if any |
190 | void nvim_ui_detach(uint64_t channel_id, Error *err) |
191 | FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY |
192 | { |
193 | if (!pmap_has(uint64_t)(connected_uis, channel_id)) { |
194 | api_set_error(err, kErrorTypeException, |
195 | "UI not attached to channel: %" PRId64, channel_id); |
196 | return; |
197 | } |
198 | remote_ui_disconnect(channel_id); |
199 | } |
200 | |
201 | |
202 | void nvim_ui_try_resize(uint64_t channel_id, Integer width, |
203 | Integer height, Error *err) |
204 | FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY |
205 | { |
206 | if (!pmap_has(uint64_t)(connected_uis, channel_id)) { |
207 | api_set_error(err, kErrorTypeException, |
208 | "UI not attached to channel: %" PRId64, channel_id); |
209 | return; |
210 | } |
211 | |
212 | if (width <= 0 || height <= 0) { |
213 | api_set_error(err, kErrorTypeValidation, |
214 | "Expected width > 0 and height > 0" ); |
215 | return; |
216 | } |
217 | |
218 | UI *ui = pmap_get(uint64_t)(connected_uis, channel_id); |
219 | ui->width = (int)width; |
220 | ui->height = (int)height; |
221 | ui_refresh(); |
222 | } |
223 | |
224 | void nvim_ui_set_option(uint64_t channel_id, String name, |
225 | Object value, Error *error) |
226 | FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY |
227 | { |
228 | if (!pmap_has(uint64_t)(connected_uis, channel_id)) { |
229 | api_set_error(error, kErrorTypeException, |
230 | "UI not attached to channel: %" PRId64, channel_id); |
231 | return; |
232 | } |
233 | UI *ui = pmap_get(uint64_t)(connected_uis, channel_id); |
234 | |
235 | ui_set_option(ui, false, name, value, error); |
236 | } |
237 | |
238 | static void ui_set_option(UI *ui, bool init, String name, Object value, |
239 | Error *error) |
240 | { |
241 | if (strequal(name.data, "override" )) { |
242 | if (value.type != kObjectTypeBoolean) { |
243 | api_set_error(error, kErrorTypeValidation, "override must be a Boolean" ); |
244 | return; |
245 | } |
246 | ui->override = value.data.boolean; |
247 | return; |
248 | } |
249 | |
250 | if (strequal(name.data, "rgb" )) { |
251 | if (value.type != kObjectTypeBoolean) { |
252 | api_set_error(error, kErrorTypeValidation, "rgb must be a Boolean" ); |
253 | return; |
254 | } |
255 | ui->rgb = value.data.boolean; |
256 | // A little drastic, but only takes effect for legacy uis. For linegrid UI |
257 | // only changes metadata for nvim_list_uis(), no refresh needed. |
258 | if (!init && !ui->ui_ext[kUILinegrid]) { |
259 | ui_refresh(); |
260 | } |
261 | return; |
262 | } |
263 | |
264 | // LEGACY: Deprecated option, use `ext_cmdline` instead. |
265 | bool = strequal(name.data, "popupmenu_external" ); |
266 | |
267 | for (UIExtension i = 0; i < kUIExtCount; i++) { |
268 | if (strequal(name.data, ui_ext_names[i]) |
269 | || (i == kUIPopupmenu && is_popupmenu)) { |
270 | if (value.type != kObjectTypeBoolean) { |
271 | api_set_error(error, kErrorTypeValidation, "%s must be a Boolean" , |
272 | name.data); |
273 | return; |
274 | } |
275 | bool boolval = value.data.boolean; |
276 | if (!init && i == kUILinegrid && boolval != ui->ui_ext[i]) { |
277 | // There shouldn't be a reason for an UI to do this ever |
278 | // so explicitly don't support this. |
279 | api_set_error(error, kErrorTypeValidation, |
280 | "ext_linegrid option cannot be changed" ); |
281 | } |
282 | ui->ui_ext[i] = boolval; |
283 | if (!init) { |
284 | ui_set_ext_option(ui, i, boolval); |
285 | } |
286 | return; |
287 | } |
288 | } |
289 | |
290 | api_set_error(error, kErrorTypeValidation, "No such UI option: %s" , |
291 | name.data); |
292 | } |
293 | |
294 | /// Tell Nvim to resize a grid. Triggers a grid_resize event with the requested |
295 | /// grid size or the maximum size if it exceeds size limits. |
296 | /// |
297 | /// On invalid grid handle, fails with error. |
298 | /// |
299 | /// @param channel_id |
300 | /// @param grid The handle of the grid to be changed. |
301 | /// @param width The new requested width. |
302 | /// @param height The new requested height. |
303 | /// @param[out] err Error details, if any |
304 | void nvim_ui_try_resize_grid(uint64_t channel_id, Integer grid, Integer width, |
305 | Integer height, Error *err) |
306 | FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY |
307 | { |
308 | if (!pmap_has(uint64_t)(connected_uis, channel_id)) { |
309 | api_set_error(err, kErrorTypeException, |
310 | "UI not attached to channel: %" PRId64, channel_id); |
311 | return; |
312 | } |
313 | |
314 | ui_grid_resize((handle_T)grid, (int)width, (int)height, err); |
315 | } |
316 | |
317 | /// Tells Nvim the number of elements displaying in the popumenu, to decide |
318 | /// <PageUp> and <PageDown> movement. |
319 | /// |
320 | /// @param channel_id |
321 | /// @param height Popupmenu height, must be greater than zero. |
322 | /// @param[out] err Error details, if any |
323 | void nvim_ui_pum_set_height(uint64_t channel_id, Integer height, Error *err) |
324 | FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY |
325 | { |
326 | if (!pmap_has(uint64_t)(connected_uis, channel_id)) { |
327 | api_set_error(err, kErrorTypeException, |
328 | "UI not attached to channel: %" PRId64, channel_id); |
329 | return; |
330 | } |
331 | |
332 | if (height <= 0) { |
333 | api_set_error(err, kErrorTypeValidation, "Expected pum height > 0" ); |
334 | return; |
335 | } |
336 | |
337 | UI *ui = pmap_get(uint64_t)(connected_uis, channel_id); |
338 | if (!ui->ui_ext[kUIPopupmenu]) { |
339 | api_set_error(err, kErrorTypeValidation, |
340 | "It must support the ext_popupmenu option" ); |
341 | return; |
342 | } |
343 | ui->pum_height = (int)height; |
344 | } |
345 | |
346 | /// Pushes data into UI.UIData, to be consumed later by remote_ui_flush(). |
347 | static void push_call(UI *ui, const char *name, Array args) |
348 | { |
349 | Array call = ARRAY_DICT_INIT; |
350 | UIData *data = ui->data; |
351 | |
352 | // To optimize data transfer(especially for "put"), we bundle adjacent |
353 | // calls to same method together, so only add a new call entry if the last |
354 | // method call is different from "name" |
355 | if (kv_size(data->buffer)) { |
356 | call = kv_A(data->buffer, kv_size(data->buffer) - 1).data.array; |
357 | } |
358 | |
359 | if (!kv_size(call) || strcmp(kv_A(call, 0).data.string.data, name)) { |
360 | call = (Array)ARRAY_DICT_INIT; |
361 | ADD(data->buffer, ARRAY_OBJ(call)); |
362 | ADD(call, STRING_OBJ(cstr_to_string(name))); |
363 | } |
364 | |
365 | ADD(call, ARRAY_OBJ(args)); |
366 | kv_A(data->buffer, kv_size(data->buffer) - 1).data.array = call; |
367 | } |
368 | |
369 | static void remote_ui_grid_clear(UI *ui, Integer grid) |
370 | { |
371 | Array args = ARRAY_DICT_INIT; |
372 | if (ui->ui_ext[kUILinegrid]) { |
373 | ADD(args, INTEGER_OBJ(grid)); |
374 | } |
375 | const char *name = ui->ui_ext[kUILinegrid] ? "grid_clear" : "clear" ; |
376 | push_call(ui, name, args); |
377 | } |
378 | |
379 | static void remote_ui_grid_resize(UI *ui, Integer grid, |
380 | Integer width, Integer height) |
381 | { |
382 | Array args = ARRAY_DICT_INIT; |
383 | if (ui->ui_ext[kUILinegrid]) { |
384 | ADD(args, INTEGER_OBJ(grid)); |
385 | } |
386 | ADD(args, INTEGER_OBJ(width)); |
387 | ADD(args, INTEGER_OBJ(height)); |
388 | const char *name = ui->ui_ext[kUILinegrid] ? "grid_resize" : "resize" ; |
389 | push_call(ui, name, args); |
390 | } |
391 | |
392 | static void remote_ui_grid_scroll(UI *ui, Integer grid, Integer top, |
393 | Integer bot, Integer left, Integer right, |
394 | Integer rows, Integer cols) |
395 | { |
396 | if (ui->ui_ext[kUILinegrid]) { |
397 | Array args = ARRAY_DICT_INIT; |
398 | ADD(args, INTEGER_OBJ(grid)); |
399 | ADD(args, INTEGER_OBJ(top)); |
400 | ADD(args, INTEGER_OBJ(bot)); |
401 | ADD(args, INTEGER_OBJ(left)); |
402 | ADD(args, INTEGER_OBJ(right)); |
403 | ADD(args, INTEGER_OBJ(rows)); |
404 | ADD(args, INTEGER_OBJ(cols)); |
405 | push_call(ui, "grid_scroll" , args); |
406 | } else { |
407 | Array args = ARRAY_DICT_INIT; |
408 | ADD(args, INTEGER_OBJ(top)); |
409 | ADD(args, INTEGER_OBJ(bot-1)); |
410 | ADD(args, INTEGER_OBJ(left)); |
411 | ADD(args, INTEGER_OBJ(right-1)); |
412 | push_call(ui, "set_scroll_region" , args); |
413 | |
414 | args = (Array)ARRAY_DICT_INIT; |
415 | ADD(args, INTEGER_OBJ(rows)); |
416 | push_call(ui, "scroll" , args); |
417 | |
418 | // some clients have "clear" being affected by scroll region, |
419 | // so reset it. |
420 | args = (Array)ARRAY_DICT_INIT; |
421 | ADD(args, INTEGER_OBJ(0)); |
422 | ADD(args, INTEGER_OBJ(ui->height-1)); |
423 | ADD(args, INTEGER_OBJ(0)); |
424 | ADD(args, INTEGER_OBJ(ui->width-1)); |
425 | push_call(ui, "set_scroll_region" , args); |
426 | } |
427 | } |
428 | |
429 | static void remote_ui_default_colors_set(UI *ui, Integer rgb_fg, |
430 | Integer rgb_bg, Integer rgb_sp, |
431 | Integer cterm_fg, Integer cterm_bg) |
432 | { |
433 | if (!ui->ui_ext[kUITermColors]) { |
434 | HL_SET_DEFAULT_COLORS(rgb_fg, rgb_bg, rgb_sp); |
435 | } |
436 | Array args = ARRAY_DICT_INIT; |
437 | ADD(args, INTEGER_OBJ(rgb_fg)); |
438 | ADD(args, INTEGER_OBJ(rgb_bg)); |
439 | ADD(args, INTEGER_OBJ(rgb_sp)); |
440 | ADD(args, INTEGER_OBJ(cterm_fg)); |
441 | ADD(args, INTEGER_OBJ(cterm_bg)); |
442 | push_call(ui, "default_colors_set" , args); |
443 | |
444 | // Deprecated |
445 | if (!ui->ui_ext[kUILinegrid]) { |
446 | args = (Array)ARRAY_DICT_INIT; |
447 | ADD(args, INTEGER_OBJ(ui->rgb ? rgb_fg : cterm_fg - 1)); |
448 | push_call(ui, "update_fg" , args); |
449 | |
450 | args = (Array)ARRAY_DICT_INIT; |
451 | ADD(args, INTEGER_OBJ(ui->rgb ? rgb_bg : cterm_bg - 1)); |
452 | push_call(ui, "update_bg" , args); |
453 | |
454 | args = (Array)ARRAY_DICT_INIT; |
455 | ADD(args, INTEGER_OBJ(ui->rgb ? rgb_sp : -1)); |
456 | push_call(ui, "update_sp" , args); |
457 | } |
458 | } |
459 | |
460 | static void remote_ui_hl_attr_define(UI *ui, Integer id, HlAttrs rgb_attrs, |
461 | HlAttrs cterm_attrs, Array info) |
462 | { |
463 | if (!ui->ui_ext[kUILinegrid]) { |
464 | return; |
465 | } |
466 | Array args = ARRAY_DICT_INIT; |
467 | |
468 | ADD(args, INTEGER_OBJ(id)); |
469 | ADD(args, DICTIONARY_OBJ(hlattrs2dict(rgb_attrs, true))); |
470 | ADD(args, DICTIONARY_OBJ(hlattrs2dict(cterm_attrs, false))); |
471 | |
472 | if (ui->ui_ext[kUIHlState]) { |
473 | ADD(args, ARRAY_OBJ(copy_array(info))); |
474 | } else { |
475 | ADD(args, ARRAY_OBJ((Array)ARRAY_DICT_INIT)); |
476 | } |
477 | |
478 | push_call(ui, "hl_attr_define" , args); |
479 | } |
480 | |
481 | static void remote_ui_highlight_set(UI *ui, int id) |
482 | { |
483 | Array args = ARRAY_DICT_INIT; |
484 | UIData *data = ui->data; |
485 | |
486 | |
487 | if (data->hl_id == id) { |
488 | return; |
489 | } |
490 | data->hl_id = id; |
491 | Dictionary hl = hlattrs2dict(syn_attr2entry(id), ui->rgb); |
492 | |
493 | ADD(args, DICTIONARY_OBJ(hl)); |
494 | push_call(ui, "highlight_set" , args); |
495 | } |
496 | |
497 | /// "true" cursor used only for input focus |
498 | static void remote_ui_grid_cursor_goto(UI *ui, Integer grid, Integer row, |
499 | Integer col) |
500 | { |
501 | if (ui->ui_ext[kUILinegrid]) { |
502 | Array args = ARRAY_DICT_INIT; |
503 | ADD(args, INTEGER_OBJ(grid)); |
504 | ADD(args, INTEGER_OBJ(row)); |
505 | ADD(args, INTEGER_OBJ(col)); |
506 | push_call(ui, "grid_cursor_goto" , args); |
507 | } else { |
508 | UIData *data = ui->data; |
509 | data->cursor_row = row; |
510 | data->cursor_col = col; |
511 | remote_ui_cursor_goto(ui, row, col); |
512 | } |
513 | } |
514 | |
515 | /// emulated cursor used both for drawing and for input focus |
516 | static void remote_ui_cursor_goto(UI *ui, Integer row, Integer col) |
517 | { |
518 | UIData *data = ui->data; |
519 | if (data->client_row == row && data->client_col == col) { |
520 | return; |
521 | } |
522 | data->client_row = row; |
523 | data->client_col = col; |
524 | Array args = ARRAY_DICT_INIT; |
525 | ADD(args, INTEGER_OBJ(row)); |
526 | ADD(args, INTEGER_OBJ(col)); |
527 | push_call(ui, "cursor_goto" , args); |
528 | } |
529 | |
530 | static void remote_ui_put(UI *ui, const char *cell) |
531 | { |
532 | UIData *data = ui->data; |
533 | data->client_col++; |
534 | Array args = ARRAY_DICT_INIT; |
535 | ADD(args, STRING_OBJ(cstr_to_string(cell))); |
536 | push_call(ui, "put" , args); |
537 | } |
538 | |
539 | static void remote_ui_raw_line(UI *ui, Integer grid, Integer row, |
540 | Integer startcol, Integer endcol, |
541 | Integer clearcol, Integer clearattr, |
542 | LineFlags flags, const schar_T *chunk, |
543 | const sattr_T *attrs) |
544 | { |
545 | UIData *data = ui->data; |
546 | if (ui->ui_ext[kUILinegrid]) { |
547 | Array args = ARRAY_DICT_INIT; |
548 | ADD(args, INTEGER_OBJ(grid)); |
549 | ADD(args, INTEGER_OBJ(row)); |
550 | ADD(args, INTEGER_OBJ(startcol)); |
551 | Array cells = ARRAY_DICT_INIT; |
552 | int repeat = 0; |
553 | size_t ncells = (size_t)(endcol-startcol); |
554 | int last_hl = -1; |
555 | for (size_t i = 0; i < ncells; i++) { |
556 | repeat++; |
557 | if (i == ncells-1 || attrs[i] != attrs[i+1] |
558 | || STRCMP(chunk[i], chunk[i+1])) { |
559 | Array cell = ARRAY_DICT_INIT; |
560 | ADD(cell, STRING_OBJ(cstr_to_string((const char *)chunk[i]))); |
561 | if (attrs[i] != last_hl || repeat > 1) { |
562 | ADD(cell, INTEGER_OBJ(attrs[i])); |
563 | last_hl = attrs[i]; |
564 | } |
565 | if (repeat > 1) { |
566 | ADD(cell, INTEGER_OBJ(repeat)); |
567 | } |
568 | ADD(cells, ARRAY_OBJ(cell)); |
569 | repeat = 0; |
570 | } |
571 | } |
572 | if (endcol < clearcol) { |
573 | Array cell = ARRAY_DICT_INIT; |
574 | ADD(cell, STRING_OBJ(cstr_to_string(" " ))); |
575 | ADD(cell, INTEGER_OBJ(clearattr)); |
576 | ADD(cell, INTEGER_OBJ(clearcol-endcol)); |
577 | ADD(cells, ARRAY_OBJ(cell)); |
578 | } |
579 | ADD(args, ARRAY_OBJ(cells)); |
580 | |
581 | push_call(ui, "grid_line" , args); |
582 | } else { |
583 | for (int i = 0; i < endcol-startcol; i++) { |
584 | remote_ui_cursor_goto(ui, row, startcol+i); |
585 | remote_ui_highlight_set(ui, attrs[i]); |
586 | remote_ui_put(ui, (const char *)chunk[i]); |
587 | if (utf_ambiguous_width(utf_ptr2char(chunk[i]))) { |
588 | data->client_col = -1; // force cursor update |
589 | } |
590 | } |
591 | if (endcol < clearcol) { |
592 | remote_ui_cursor_goto(ui, row, endcol); |
593 | remote_ui_highlight_set(ui, (int)clearattr); |
594 | // legacy eol_clear was only ever used with cleared attributes |
595 | // so be on the safe side |
596 | if (clearattr == 0 && clearcol == Columns) { |
597 | Array args = ARRAY_DICT_INIT; |
598 | push_call(ui, "eol_clear" , args); |
599 | } else { |
600 | for (Integer c = endcol; c < clearcol; c++) { |
601 | remote_ui_put(ui, " " ); |
602 | } |
603 | } |
604 | } |
605 | } |
606 | } |
607 | |
608 | static void remote_ui_flush(UI *ui) |
609 | { |
610 | UIData *data = ui->data; |
611 | if (data->buffer.size > 0) { |
612 | if (!ui->ui_ext[kUILinegrid]) { |
613 | remote_ui_cursor_goto(ui, data->cursor_row, data->cursor_col); |
614 | } |
615 | push_call(ui, "flush" , (Array)ARRAY_DICT_INIT); |
616 | rpc_send_event(data->channel_id, "redraw" , data->buffer); |
617 | data->buffer = (Array)ARRAY_DICT_INIT; |
618 | } |
619 | } |
620 | |
621 | static Array translate_contents(UI *ui, Array contents) |
622 | { |
623 | Array new_contents = ARRAY_DICT_INIT; |
624 | for (size_t i = 0; i < contents.size; i++) { |
625 | Array item = contents.items[i].data.array; |
626 | Array new_item = ARRAY_DICT_INIT; |
627 | int attr = (int)item.items[0].data.integer; |
628 | if (attr) { |
629 | Dictionary rgb_attrs = hlattrs2dict(syn_attr2entry(attr), ui->rgb); |
630 | ADD(new_item, DICTIONARY_OBJ(rgb_attrs)); |
631 | } else { |
632 | ADD(new_item, DICTIONARY_OBJ((Dictionary)ARRAY_DICT_INIT)); |
633 | } |
634 | ADD(new_item, copy_object(item.items[1])); |
635 | ADD(new_contents, ARRAY_OBJ(new_item)); |
636 | } |
637 | return new_contents; |
638 | } |
639 | |
640 | static Array translate_firstarg(UI *ui, Array args) |
641 | { |
642 | Array new_args = ARRAY_DICT_INIT; |
643 | Array contents = args.items[0].data.array; |
644 | |
645 | ADD(new_args, ARRAY_OBJ(translate_contents(ui, contents))); |
646 | for (size_t i = 1; i < args.size; i++) { |
647 | ADD(new_args, copy_object(args.items[i])); |
648 | } |
649 | return new_args; |
650 | } |
651 | |
652 | static void remote_ui_event(UI *ui, char *name, Array args, bool *args_consumed) |
653 | { |
654 | UIData *data = ui->data; |
655 | if (!ui->ui_ext[kUILinegrid]) { |
656 | // the representation of highlights in cmdline changed, translate back |
657 | // never consumes args |
658 | if (strequal(name, "cmdline_show" )) { |
659 | Array new_args = translate_firstarg(ui, args); |
660 | push_call(ui, name, new_args); |
661 | return; |
662 | } else if (strequal(name, "cmdline_block_show" )) { |
663 | Array new_args = ARRAY_DICT_INIT; |
664 | Array block = args.items[0].data.array; |
665 | Array new_block = ARRAY_DICT_INIT; |
666 | for (size_t i = 0; i < block.size; i++) { |
667 | ADD(new_block, |
668 | ARRAY_OBJ(translate_contents(ui, block.items[i].data.array))); |
669 | } |
670 | ADD(new_args, ARRAY_OBJ(new_block)); |
671 | push_call(ui, name, new_args); |
672 | return; |
673 | } else if (strequal(name, "cmdline_block_append" )) { |
674 | Array new_args = translate_firstarg(ui, args); |
675 | push_call(ui, name, new_args); |
676 | return; |
677 | } |
678 | } |
679 | |
680 | // Back-compat: translate popupmenu_xx to legacy wildmenu_xx. |
681 | if (ui->ui_ext[kUIWildmenu]) { |
682 | if (strequal(name, "popupmenu_show" )) { |
683 | data->wildmenu_active = (args.items[4].data.integer == -1) |
684 | || !ui->ui_ext[kUIPopupmenu]; |
685 | if (data->wildmenu_active) { |
686 | Array new_args = ARRAY_DICT_INIT; |
687 | Array items = args.items[0].data.array; |
688 | Array new_items = ARRAY_DICT_INIT; |
689 | for (size_t i = 0; i < items.size; i++) { |
690 | ADD(new_items, copy_object(items.items[i].data.array.items[0])); |
691 | } |
692 | ADD(new_args, ARRAY_OBJ(new_items)); |
693 | push_call(ui, "wildmenu_show" , new_args); |
694 | if (args.items[1].data.integer != -1) { |
695 | Array new_args2 = ARRAY_DICT_INIT; |
696 | ADD(new_args2, args.items[1]); |
697 | push_call(ui, "wildmenu_select" , new_args); |
698 | } |
699 | return; |
700 | } |
701 | } else if (strequal(name, "popupmenu_select" )) { |
702 | if (data->wildmenu_active) { |
703 | name = "wildmenu_select" ; |
704 | } |
705 | } else if (strequal(name, "popupmenu_hide" )) { |
706 | if (data->wildmenu_active) { |
707 | name = "wildmenu_hide" ; |
708 | } |
709 | } |
710 | } |
711 | |
712 | |
713 | Array my_args = ARRAY_DICT_INIT; |
714 | // Objects are currently single-reference |
715 | // make a copy, but only if necessary |
716 | if (*args_consumed) { |
717 | for (size_t i = 0; i < args.size; i++) { |
718 | ADD(my_args, copy_object(args.items[i])); |
719 | } |
720 | } else { |
721 | my_args = args; |
722 | *args_consumed = true; |
723 | } |
724 | push_call(ui, name, my_args); |
725 | } |
726 | |
727 | static void remote_ui_inspect(UI *ui, Dictionary *info) |
728 | { |
729 | UIData *data = ui->data; |
730 | PUT(*info, "chan" , INTEGER_OBJ((Integer)data->channel_id)); |
731 | } |
732 | |