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 <inttypes.h>
6#include <stdbool.h>
7#include <string.h>
8#include <limits.h>
9
10#include "nvim/vim.h"
11#include "nvim/log.h"
12#include "nvim/aucmd.h"
13#include "nvim/ui.h"
14#include "nvim/charset.h"
15#include "nvim/cursor.h"
16#include "nvim/diff.h"
17#include "nvim/ex_cmds2.h"
18#include "nvim/ex_getln.h"
19#include "nvim/fold.h"
20#include "nvim/main.h"
21#include "nvim/ascii.h"
22#include "nvim/misc1.h"
23#include "nvim/mbyte.h"
24#include "nvim/garray.h"
25#include "nvim/memory.h"
26#include "nvim/move.h"
27#include "nvim/normal.h"
28#include "nvim/option.h"
29#include "nvim/os_unix.h"
30#include "nvim/event/loop.h"
31#include "nvim/os/time.h"
32#include "nvim/os/input.h"
33#include "nvim/os/signal.h"
34#include "nvim/popupmnu.h"
35#include "nvim/screen.h"
36#include "nvim/highlight.h"
37#include "nvim/ui_compositor.h"
38#include "nvim/window.h"
39#include "nvim/cursor_shape.h"
40#ifdef FEAT_TUI
41# include "nvim/tui/tui.h"
42#else
43# include "nvim/msgpack_rpc/server.h"
44#endif
45#include "nvim/api/private/helpers.h"
46
47#ifdef INCLUDE_GENERATED_DECLARATIONS
48# include "ui.c.generated.h"
49#endif
50
51#define MAX_UI_COUNT 16
52
53static UI *uis[MAX_UI_COUNT];
54static bool ui_ext[kUIExtCount] = { 0 };
55static size_t ui_count = 0;
56static int ui_mode_idx = SHAPE_IDX_N;
57static int cursor_row = 0, cursor_col = 0;
58static bool pending_cursor_update = false;
59static int busy = 0;
60static bool pending_mode_info_update = false;
61static bool pending_mode_update = false;
62static handle_T cursor_grid_handle = DEFAULT_GRID_HANDLE;
63
64#if MIN_LOG_LEVEL > DEBUG_LOG_LEVEL
65# define UI_LOG(funname)
66#else
67static size_t uilog_seen = 0;
68static char uilog_last_event[1024] = { 0 };
69# define UI_LOG(funname) \
70 do { \
71 if (strequal(uilog_last_event, STR(funname))) { \
72 uilog_seen++; \
73 } else { \
74 if (uilog_seen > 0) { \
75 logmsg(DEBUG_LOG_LEVEL, "UI: ", NULL, -1, true, \
76 "%s (+%zu times...)", uilog_last_event, uilog_seen); \
77 } \
78 logmsg(DEBUG_LOG_LEVEL, "UI: ", NULL, -1, true, STR(funname)); \
79 uilog_seen = 0; \
80 xstrlcpy(uilog_last_event, STR(funname), sizeof(uilog_last_event)); \
81 } \
82 } while (0)
83#endif
84
85// UI_CALL invokes a function on all registered UI instances.
86// This is called by code generated by generators/gen_api_ui_events.lua
87// C code should use ui_call_{funname} instead.
88# define UI_CALL(cond, funname, ...) \
89 do { \
90 bool any_call = false; \
91 for (size_t i = 0; i < ui_count; i++) { \
92 UI *ui = uis[i]; \
93 if (ui->funname && (cond)) { \
94 ui->funname(__VA_ARGS__); \
95 any_call = true; \
96 } \
97 } \
98 if (any_call) { \
99 UI_LOG(funname); \
100 } \
101 } while (0)
102
103#ifdef INCLUDE_GENERATED_DECLARATIONS
104# include "ui_events_call.generated.h"
105#endif
106
107void ui_init(void)
108{
109 default_grid.handle = 1;
110 ui_comp_init();
111}
112
113void ui_builtin_start(void)
114{
115#ifdef FEAT_TUI
116 tui_start();
117#else
118 fprintf(stderr, "Nvim headless-mode started.\n");
119 size_t len;
120 char **addrs = server_address_list(&len);
121 if (addrs != NULL) {
122 fprintf(stderr, "Listening on:\n");
123 for (size_t i = 0; i < len; i++) {
124 fprintf(stderr, "\t%s\n", addrs[i]);
125 }
126 xfree(addrs);
127 }
128 fprintf(stderr, "Press CTRL+C to exit.\n");
129#endif
130}
131
132bool ui_rgb_attached(void)
133{
134 if (!headless_mode && p_tgc) {
135 return true;
136 }
137 for (size_t i = 1; i < ui_count; i++) {
138 if (uis[i]->rgb) {
139 return true;
140 }
141 }
142 return false;
143}
144
145/// Returns true if any UI requested `override=true`.
146bool ui_override(void)
147{
148 for (size_t i = 1; i < ui_count; i++) {
149 if (uis[i]->override) {
150 return true;
151 }
152 }
153 return false;
154}
155
156bool ui_active(void)
157{
158 return ui_count > 1;
159}
160
161void ui_event(char *name, Array args)
162{
163 bool args_consumed = false;
164 ui_call_event(name, args, &args_consumed);
165 if (!args_consumed) {
166 api_free_array(args);
167 }
168}
169
170
171void ui_refresh(void)
172{
173 if (!ui_active()) {
174 return;
175 }
176
177 if (updating_screen) {
178 deferred_refresh_event(NULL);
179 return;
180 }
181
182 int width = INT_MAX, height = INT_MAX;
183 bool ext_widgets[kUIExtCount];
184 for (UIExtension i = 0; (int)i < kUIExtCount; i++) {
185 ext_widgets[i] = true;
186 }
187
188 bool inclusive = ui_override();
189 for (size_t i = 0; i < ui_count; i++) {
190 UI *ui = uis[i];
191 width = MIN(ui->width, width);
192 height = MIN(ui->height, height);
193 for (UIExtension j = 0; (int)j < kUIExtCount; j++) {
194 ext_widgets[j] &= (ui->ui_ext[j] || inclusive);
195 }
196 }
197
198 cursor_row = cursor_col = 0;
199 pending_cursor_update = true;
200
201 for (UIExtension i = 0; (int)i < kUIExtCount; i++) {
202 ui_ext[i] = ext_widgets[i];
203 if (i < kUIGlobalCount) {
204 ui_call_option_set(cstr_as_string((char *)ui_ext_names[i]),
205 BOOLEAN_OBJ(ext_widgets[i]));
206 }
207 }
208
209 ui_default_colors_set();
210
211 int save_p_lz = p_lz;
212 p_lz = false; // convince redrawing() to return true ...
213 screen_resize(width, height);
214 p_lz = save_p_lz;
215
216 if (ext_widgets[kUIMessages]) {
217 p_ch = 0;
218 command_height();
219 }
220 ui_mode_info_set();
221 pending_mode_update = true;
222 ui_cursor_shape();
223}
224
225int ui_pum_get_height(void)
226{
227 int pum_height = 0;
228 for (size_t i = 1; i < ui_count; i++) {
229 int ui_pum_height = uis[i]->pum_height;
230 if (ui_pum_height) {
231 pum_height =
232 pum_height != 0 ? MIN(pum_height, ui_pum_height) : ui_pum_height;
233 }
234 }
235 return pum_height;
236}
237
238static void ui_refresh_event(void **argv)
239{
240 ui_refresh();
241}
242
243void ui_schedule_refresh(void)
244{
245 loop_schedule_fast(&main_loop, event_create(deferred_refresh_event, 0));
246}
247static void deferred_refresh_event(void **argv)
248{
249 multiqueue_put(resize_events, ui_refresh_event, 0);
250}
251
252void ui_default_colors_set(void)
253{
254 ui_call_default_colors_set(normal_fg, normal_bg, normal_sp,
255 cterm_normal_fg_color, cterm_normal_bg_color);
256}
257
258void ui_busy_start(void)
259{
260 if (!(busy++)) {
261 ui_call_busy_start();
262 }
263}
264
265void ui_busy_stop(void)
266{
267 if (!(--busy)) {
268 ui_call_busy_stop();
269 }
270}
271
272void ui_attach_impl(UI *ui, uint64_t chanid)
273{
274 if (ui_count == MAX_UI_COUNT) {
275 abort();
276 }
277 if (!ui->ui_ext[kUIMultigrid] && !ui->ui_ext[kUIFloatDebug]) {
278 ui_comp_attach(ui);
279 }
280
281 uis[ui_count++] = ui;
282 ui_refresh_options();
283
284 for (UIExtension i = kUIGlobalCount; (int)i < kUIExtCount; i++) {
285 ui_set_ext_option(ui, i, ui->ui_ext[i]);
286 }
287
288 bool sent = false;
289 if (ui->ui_ext[kUIHlState]) {
290 sent = highlight_use_hlstate();
291 }
292 if (!sent) {
293 ui_send_all_hls(ui);
294 }
295 ui_refresh();
296
297 bool is_compositor = (ui == uis[0]);
298 if (!is_compositor) {
299 do_autocmd_uienter(chanid, true);
300 }
301}
302
303void ui_detach_impl(UI *ui, uint64_t chanid)
304{
305 size_t shift_index = MAX_UI_COUNT;
306
307 // Find the index that will be removed
308 for (size_t i = 0; i < ui_count; i++) {
309 if (uis[i] == ui) {
310 shift_index = i;
311 break;
312 }
313 }
314
315 if (shift_index == MAX_UI_COUNT) {
316 abort();
317 }
318
319 // Shift UIs at "shift_index"
320 while (shift_index < ui_count - 1) {
321 uis[shift_index] = uis[shift_index + 1];
322 shift_index++;
323 }
324
325 if (--ui_count
326 // During teardown/exit the loop was already destroyed, cannot schedule.
327 // https://github.com/neovim/neovim/pull/5119#issuecomment-258667046
328 && !exiting) {
329 ui_schedule_refresh();
330 }
331
332 if (!ui->ui_ext[kUIMultigrid] && !ui->ui_ext[kUIFloatDebug]) {
333 ui_comp_detach(ui);
334 }
335
336 do_autocmd_uienter(chanid, false);
337}
338
339void ui_set_ext_option(UI *ui, UIExtension ext, bool active)
340{
341 if (ext < kUIGlobalCount) {
342 ui_refresh();
343 return;
344 }
345 if (ui->option_set && (ui_ext_names[ext][0] != '_' || active)) {
346 ui->option_set(ui, cstr_as_string((char *)ui_ext_names[ext]),
347 BOOLEAN_OBJ(active));
348 }
349 if (ext == kUITermColors) {
350 ui_default_colors_set();
351 }
352}
353
354void ui_line(ScreenGrid *grid, int row, int startcol, int endcol, int clearcol,
355 int clearattr, bool wrap)
356{
357 assert(0 <= row && row < grid->Rows);
358 LineFlags flags = wrap ? kLineFlagWrap : 0;
359 if (startcol == -1) {
360 startcol = 0;
361 flags |= kLineFlagInvalid;
362 }
363
364 size_t off = grid->line_offset[row] + (size_t)startcol;
365
366 ui_call_raw_line(grid->handle, row, startcol, endcol, clearcol, clearattr,
367 flags, (const schar_T *)grid->chars + off,
368 (const sattr_T *)grid->attrs + off);
369
370 // 'writedelay': flush & delay each time.
371 if (p_wd && !(rdb_flags & RDB_COMPOSITOR)) {
372 // If 'writedelay' is active, set the cursor to indicate what was drawn.
373 ui_call_grid_cursor_goto(grid->handle, row,
374 MIN(clearcol, (int)grid->Columns-1));
375 ui_call_flush();
376 uint64_t wd = (uint64_t)labs(p_wd);
377 os_microdelay(wd * 1000u, true);
378 pending_cursor_update = true; // restore the cursor later
379 }
380}
381
382void ui_cursor_goto(int new_row, int new_col)
383{
384 ui_grid_cursor_goto(DEFAULT_GRID_HANDLE, new_row, new_col);
385}
386
387void ui_grid_cursor_goto(handle_T grid_handle, int new_row, int new_col)
388{
389 if (new_row == cursor_row
390 && new_col == cursor_col
391 && grid_handle == cursor_grid_handle) {
392 return;
393 }
394
395 cursor_row = new_row;
396 cursor_col = new_col;
397 cursor_grid_handle = grid_handle;
398 pending_cursor_update = true;
399}
400
401/// moving the cursor grid will implicitly move the cursor
402void ui_check_cursor_grid(handle_T grid_handle)
403{
404 if (cursor_grid_handle == grid_handle) {
405 pending_cursor_update = true;
406 }
407}
408
409void ui_mode_info_set(void)
410{
411 pending_mode_info_update = true;
412}
413
414int ui_current_row(void)
415{
416 return cursor_row;
417}
418
419int ui_current_col(void)
420{
421 return cursor_col;
422}
423
424void ui_flush(void)
425{
426 cmdline_ui_flush();
427 win_ui_flush_positions();
428 msg_ext_ui_flush();
429 msg_scroll_flush();
430
431 if (pending_cursor_update) {
432 ui_call_grid_cursor_goto(cursor_grid_handle, cursor_row, cursor_col);
433 pending_cursor_update = false;
434 }
435 if (pending_mode_info_update) {
436 Array style = mode_style_array();
437 bool enabled = (*p_guicursor != NUL);
438 ui_call_mode_info_set(enabled, style);
439 api_free_array(style);
440 pending_mode_info_update = false;
441 }
442 if (pending_mode_update) {
443 char *full_name = shape_table[ui_mode_idx].full_name;
444 ui_call_mode_change(cstr_as_string(full_name), ui_mode_idx);
445 pending_mode_update = false;
446 }
447 ui_call_flush();
448}
449
450/// Check if current mode has changed.
451/// May update the shape of the cursor.
452void ui_cursor_shape(void)
453{
454 if (!full_screen) {
455 return;
456 }
457 int new_mode_idx = cursor_get_mode_idx();
458
459 if (new_mode_idx != ui_mode_idx) {
460 ui_mode_idx = new_mode_idx;
461 pending_mode_update = true;
462 }
463 conceal_check_cursor_line();
464}
465
466/// Returns true if the given UI extension is enabled.
467bool ui_has(UIExtension ext)
468{
469 return ui_ext[ext];
470}
471
472Array ui_array(void)
473{
474 Array all_uis = ARRAY_DICT_INIT;
475 for (size_t i = 1; i < ui_count; i++) {
476 UI *ui = uis[i];
477 Dictionary info = ARRAY_DICT_INIT;
478 PUT(info, "width", INTEGER_OBJ(ui->width));
479 PUT(info, "height", INTEGER_OBJ(ui->height));
480 PUT(info, "rgb", BOOLEAN_OBJ(ui->rgb));
481 PUT(info, "override", BOOLEAN_OBJ(ui->override));
482 for (UIExtension j = 0; j < kUIExtCount; j++) {
483 if (ui_ext_names[j][0] != '_' || ui->ui_ext[j]) {
484 PUT(info, ui_ext_names[j], BOOLEAN_OBJ(ui->ui_ext[j]));
485 }
486 }
487 ui->inspect(ui, &info);
488 ADD(all_uis, DICTIONARY_OBJ(info));
489 }
490 return all_uis;
491}
492
493void ui_grid_resize(handle_T grid_handle, int width, int height, Error *error)
494{
495 if (grid_handle == DEFAULT_GRID_HANDLE) {
496 screen_resize(width, height);
497 return;
498 }
499
500 win_T *wp = get_win_by_grid_handle(grid_handle);
501 if (wp == NULL) {
502 api_set_error(error, kErrorTypeValidation,
503 "No window with the given handle");
504 return;
505 }
506
507 if (wp->w_floating) {
508 if (width != wp->w_width && height != wp->w_height) {
509 wp->w_float_config.width = width;
510 wp->w_float_config.height = height;
511 win_config_float(wp, wp->w_float_config);
512 }
513 } else {
514 // non-positive indicates no request
515 wp->w_height_request = (int)MAX(height, 0);
516 wp->w_width_request = (int)MAX(width, 0);
517 win_set_inner_size(wp);
518 }
519}
520