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 | |
53 | static UI *uis[MAX_UI_COUNT]; |
54 | static bool ui_ext[kUIExtCount] = { 0 }; |
55 | static size_t ui_count = 0; |
56 | static int ui_mode_idx = SHAPE_IDX_N; |
57 | static int cursor_row = 0, cursor_col = 0; |
58 | static bool pending_cursor_update = false; |
59 | static int busy = 0; |
60 | static bool pending_mode_info_update = false; |
61 | static bool pending_mode_update = false; |
62 | static handle_T cursor_grid_handle = DEFAULT_GRID_HANDLE; |
63 | |
64 | #if MIN_LOG_LEVEL > DEBUG_LOG_LEVEL |
65 | # define UI_LOG(funname) |
66 | #else |
67 | static size_t uilog_seen = 0; |
68 | static 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 | |
107 | void ui_init(void) |
108 | { |
109 | default_grid.handle = 1; |
110 | ui_comp_init(); |
111 | } |
112 | |
113 | void 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 | |
132 | bool 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`. |
146 | bool 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 | |
156 | bool ui_active(void) |
157 | { |
158 | return ui_count > 1; |
159 | } |
160 | |
161 | void 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 | |
171 | void 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 | |
225 | int 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 | |
238 | static void ui_refresh_event(void **argv) |
239 | { |
240 | ui_refresh(); |
241 | } |
242 | |
243 | void ui_schedule_refresh(void) |
244 | { |
245 | loop_schedule_fast(&main_loop, event_create(deferred_refresh_event, 0)); |
246 | } |
247 | static void deferred_refresh_event(void **argv) |
248 | { |
249 | multiqueue_put(resize_events, ui_refresh_event, 0); |
250 | } |
251 | |
252 | void 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 | |
258 | void ui_busy_start(void) |
259 | { |
260 | if (!(busy++)) { |
261 | ui_call_busy_start(); |
262 | } |
263 | } |
264 | |
265 | void ui_busy_stop(void) |
266 | { |
267 | if (!(--busy)) { |
268 | ui_call_busy_stop(); |
269 | } |
270 | } |
271 | |
272 | void 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 | |
303 | void 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 | |
339 | void 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 | |
354 | void 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 | |
382 | void ui_cursor_goto(int new_row, int new_col) |
383 | { |
384 | ui_grid_cursor_goto(DEFAULT_GRID_HANDLE, new_row, new_col); |
385 | } |
386 | |
387 | void 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 |
402 | void ui_check_cursor_grid(handle_T grid_handle) |
403 | { |
404 | if (cursor_grid_handle == grid_handle) { |
405 | pending_cursor_update = true; |
406 | } |
407 | } |
408 | |
409 | void ui_mode_info_set(void) |
410 | { |
411 | pending_mode_info_update = true; |
412 | } |
413 | |
414 | int ui_current_row(void) |
415 | { |
416 | return cursor_row; |
417 | } |
418 | |
419 | int ui_current_col(void) |
420 | { |
421 | return cursor_col; |
422 | } |
423 | |
424 | void 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. |
452 | void 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. |
467 | bool ui_has(UIExtension ext) |
468 | { |
469 | return ui_ext[ext]; |
470 | } |
471 | |
472 | Array 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 | |
493 | void 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 | |