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 | /* |
5 | * Code for menus. Used for the GUI and 'wildmenu'. |
6 | * GUI/Motif support by Robert Webb |
7 | */ |
8 | |
9 | #include <assert.h> |
10 | #include <inttypes.h> |
11 | #include <string.h> |
12 | |
13 | #include "nvim/vim.h" |
14 | #include "nvim/ascii.h" |
15 | #include "nvim/menu.h" |
16 | #include "nvim/charset.h" |
17 | #include "nvim/cursor.h" |
18 | #include "nvim/eval.h" |
19 | #include "nvim/ex_docmd.h" |
20 | #include "nvim/getchar.h" |
21 | #include "nvim/memory.h" |
22 | #include "nvim/message.h" |
23 | #include "nvim/misc1.h" |
24 | #include "nvim/keymap.h" |
25 | #include "nvim/garray.h" |
26 | #include "nvim/state.h" |
27 | #include "nvim/strings.h" |
28 | #include "nvim/ui.h" |
29 | #include "nvim/eval/typval.h" |
30 | |
31 | #define 10 /* maximum depth of menus */ |
32 | |
33 | |
34 | #ifdef INCLUDE_GENERATED_DECLARATIONS |
35 | # include "menu.c.generated.h" |
36 | #endif |
37 | |
38 | |
39 | |
40 | |
41 | /// The character for each menu mode |
42 | static char_u [] = { 'n', 'v', 's', 'o', 'i', 'c', 't' }; |
43 | |
44 | static char_u [] = N_( |
45 | "E327: Part of menu-item path is not sub-menu" ); |
46 | static char_u e_othermode[] = N_("E328: Menu only exists in another mode" ); |
47 | static char_u [] = N_("E329: No menu \"%s\"" ); |
48 | |
49 | |
50 | /// Do the :menu command and relatives. |
51 | /// @param eap Ex command arguments |
52 | void |
53 | (exarg_T *eap) |
54 | { |
55 | char_u *; |
56 | int modes; |
57 | char_u *map_to; // command mapped to the menu entry |
58 | int noremap; |
59 | bool silent = false; |
60 | int ; |
61 | char_u *map_buf; |
62 | char_u *arg; |
63 | char_u *p; |
64 | int i; |
65 | long pri_tab[MENUDEPTH + 1]; |
66 | TriState enable = kNone; // kTrue for "menu enable", |
67 | // kFalse for "menu disable |
68 | vimmenu_T ; |
69 | |
70 | modes = get_menu_cmd_modes(eap->cmd, eap->forceit, &noremap, &unmenu); |
71 | arg = eap->arg; |
72 | |
73 | for (;; ) { |
74 | if (STRNCMP(arg, "<script>" , 8) == 0) { |
75 | noremap = REMAP_SCRIPT; |
76 | arg = skipwhite(arg + 8); |
77 | continue; |
78 | } |
79 | if (STRNCMP(arg, "<silent>" , 8) == 0) { |
80 | silent = true; |
81 | arg = skipwhite(arg + 8); |
82 | continue; |
83 | } |
84 | if (STRNCMP(arg, "<special>" , 9) == 0) { |
85 | // Ignore obsolete "<special>" modifier. |
86 | arg = skipwhite(arg + 9); |
87 | continue; |
88 | } |
89 | break; |
90 | } |
91 | |
92 | |
93 | // Locate an optional "icon=filename" argument |
94 | // TODO(nvim): Currently this is only parsed. Should expose it to UIs. |
95 | if (STRNCMP(arg, "icon=" , 5) == 0) { |
96 | arg += 5; |
97 | while (*arg != NUL && *arg != ' ') { |
98 | if (*arg == '\\') |
99 | STRMOVE(arg, arg + 1); |
100 | MB_PTR_ADV(arg); |
101 | } |
102 | if (*arg != NUL) { |
103 | *arg++ = NUL; |
104 | arg = skipwhite(arg); |
105 | } |
106 | } |
107 | |
108 | // Fill in the priority table. |
109 | for (p = arg; *p; p++) { |
110 | if (!ascii_isdigit(*p) && *p != '.') { |
111 | break; |
112 | } |
113 | } |
114 | if (ascii_iswhite(*p)) { |
115 | for (i = 0; i < MENUDEPTH && !ascii_iswhite(*arg); i++) { |
116 | pri_tab[i] = getdigits_long(&arg, false, 0); |
117 | if (pri_tab[i] == 0) { |
118 | pri_tab[i] = 500; |
119 | } |
120 | if (*arg == '.') { |
121 | arg++; |
122 | } |
123 | } |
124 | arg = skipwhite(arg); |
125 | } else if (eap->addr_count && eap->line2 != 0) { |
126 | pri_tab[0] = eap->line2; |
127 | i = 1; |
128 | } else |
129 | i = 0; |
130 | while (i < MENUDEPTH) |
131 | pri_tab[i++] = 500; |
132 | pri_tab[MENUDEPTH] = -1; /* mark end of the table */ |
133 | |
134 | /* |
135 | * Check for "disable" or "enable" argument. |
136 | */ |
137 | if (STRNCMP(arg, "enable" , 6) == 0 && ascii_iswhite(arg[6])) { |
138 | enable = kTrue; |
139 | arg = skipwhite(arg + 6); |
140 | } else if (STRNCMP(arg, "disable" , 7) == 0 && ascii_iswhite(arg[7])) { |
141 | enable = kFalse; |
142 | arg = skipwhite(arg + 7); |
143 | } |
144 | |
145 | /* |
146 | * If there is no argument, display all menus. |
147 | */ |
148 | if (*arg == NUL) { |
149 | show_menus(arg, modes); |
150 | return; |
151 | } |
152 | |
153 | |
154 | menu_path = arg; |
155 | if (*menu_path == '.') { |
156 | EMSG2(_(e_invarg2), menu_path); |
157 | goto theend; |
158 | } |
159 | |
160 | map_to = menu_translate_tab_and_shift(arg); |
161 | |
162 | /* |
163 | * If there is only a menu name, display menus with that name. |
164 | */ |
165 | if (*map_to == NUL && !unmenu && enable == kNone) { |
166 | show_menus(menu_path, modes); |
167 | goto theend; |
168 | } else if (*map_to != NUL && (unmenu || enable != kNone)) { |
169 | EMSG(_(e_trailing)); |
170 | goto theend; |
171 | } |
172 | |
173 | if (enable != kNone) { |
174 | // Change sensitivity of the menu. |
175 | // For the PopUp menu, remove a menu for each mode separately. |
176 | // Careful: menu_enable_recurse() changes menu_path. |
177 | if (STRCMP(menu_path, "*" ) == 0) { // meaning: do all menus |
178 | menu_path = (char_u *)"" ; |
179 | } |
180 | |
181 | if (menu_is_popup(menu_path)) { |
182 | for (i = 0; i < MENU_INDEX_TIP; ++i) |
183 | if (modes & (1 << i)) { |
184 | p = popup_mode_name(menu_path, i); |
185 | menu_enable_recurse(root_menu, p, MENU_ALL_MODES, enable); |
186 | xfree(p); |
187 | } |
188 | } |
189 | menu_enable_recurse(root_menu, menu_path, modes, enable); |
190 | } else if (unmenu) { |
191 | /* |
192 | * Delete menu(s). |
193 | */ |
194 | if (STRCMP(menu_path, "*" ) == 0) /* meaning: remove all menus */ |
195 | menu_path = (char_u *)"" ; |
196 | |
197 | /* |
198 | * For the PopUp menu, remove a menu for each mode separately. |
199 | */ |
200 | if (menu_is_popup(menu_path)) { |
201 | for (i = 0; i < MENU_INDEX_TIP; ++i) |
202 | if (modes & (1 << i)) { |
203 | p = popup_mode_name(menu_path, i); |
204 | remove_menu(&root_menu, p, MENU_ALL_MODES, TRUE); |
205 | xfree(p); |
206 | } |
207 | } |
208 | |
209 | /* Careful: remove_menu() changes menu_path */ |
210 | remove_menu(&root_menu, menu_path, modes, FALSE); |
211 | } else { |
212 | /* |
213 | * Add menu(s). |
214 | * Replace special key codes. |
215 | */ |
216 | if (STRICMP(map_to, "<nop>" ) == 0) { /* "<Nop>" means nothing */ |
217 | map_to = (char_u *)"" ; |
218 | map_buf = NULL; |
219 | } else if (modes & MENU_TIP_MODE) { |
220 | map_buf = NULL; // Menu tips are plain text. |
221 | } else { |
222 | map_to = replace_termcodes(map_to, STRLEN(map_to), &map_buf, false, true, |
223 | true, CPO_TO_CPO_FLAGS); |
224 | } |
225 | menuarg.modes = modes; |
226 | menuarg.noremap[0] = noremap; |
227 | menuarg.silent[0] = silent; |
228 | add_menu_path(menu_path, &menuarg, pri_tab, map_to); |
229 | |
230 | /* |
231 | * For the PopUp menu, add a menu for each mode separately. |
232 | */ |
233 | if (menu_is_popup(menu_path)) { |
234 | for (i = 0; i < MENU_INDEX_TIP; ++i) |
235 | if (modes & (1 << i)) { |
236 | p = popup_mode_name(menu_path, i); |
237 | // Include all modes, to make ":amenu" work |
238 | menuarg.modes = modes; |
239 | add_menu_path(p, &menuarg, pri_tab, map_to); |
240 | xfree(p); |
241 | } |
242 | } |
243 | |
244 | xfree(map_buf); |
245 | } |
246 | |
247 | ui_call_update_menu(); |
248 | |
249 | theend: |
250 | ; |
251 | } |
252 | |
253 | |
254 | /// Add the menu with the given name to the menu hierarchy |
255 | /// |
256 | /// @param[out] menuarg menu entry |
257 | /// @param[] pri_tab priority table |
258 | /// @param[in] call_data Right hand side command |
259 | static int |
260 | ( |
261 | const char_u *const , |
262 | vimmenu_T *, |
263 | const long *const pri_tab, |
264 | const char_u *const call_data |
265 | ) |
266 | { |
267 | char_u *path_name; |
268 | int modes = menuarg->modes; |
269 | vimmenu_T **; |
270 | vimmenu_T * = NULL; |
271 | vimmenu_T *parent; |
272 | vimmenu_T **lower_pri; |
273 | char_u *p; |
274 | char_u *name; |
275 | char_u *dname; |
276 | char_u *next_name; |
277 | char_u c; |
278 | char_u d; |
279 | int i; |
280 | int pri_idx = 0; |
281 | int old_modes = 0; |
282 | int ; |
283 | char_u *en_name; |
284 | char_u *map_to = NULL; |
285 | |
286 | /* Make a copy so we can stuff around with it, since it could be const */ |
287 | path_name = vim_strsave(menu_path); |
288 | menup = &root_menu; |
289 | parent = NULL; |
290 | name = path_name; |
291 | while (*name) { |
292 | /* Get name of this element in the menu hierarchy, and the simplified |
293 | * name (without mnemonic and accelerator text). */ |
294 | next_name = menu_name_skip(name); |
295 | map_to = menutrans_lookup(name, (int)STRLEN(name)); |
296 | if (map_to != NULL) { |
297 | en_name = name; |
298 | name = map_to; |
299 | } else { |
300 | en_name = NULL; |
301 | } |
302 | dname = menu_text(name, NULL, NULL); |
303 | if (*dname == NUL) { |
304 | /* Only a mnemonic or accelerator is not valid. */ |
305 | EMSG(_("E792: Empty menu name" )); |
306 | goto erret; |
307 | } |
308 | |
309 | /* See if it's already there */ |
310 | lower_pri = menup; |
311 | menu = *menup; |
312 | while (menu != NULL) { |
313 | if (menu_name_equal(name, menu) || menu_name_equal(dname, menu)) { |
314 | if (*next_name == NUL && menu->children != NULL) { |
315 | if (!sys_menu) { |
316 | EMSG(_("E330: Menu path must not lead to a sub-menu" )); |
317 | } |
318 | goto erret; |
319 | } |
320 | if (*next_name != NUL && menu->children == NULL) { |
321 | if (!sys_menu) { |
322 | EMSG(_(e_notsubmenu)); |
323 | } |
324 | goto erret; |
325 | } |
326 | break; |
327 | } |
328 | menup = &menu->next; |
329 | |
330 | /* Count menus, to find where this one needs to be inserted. |
331 | * Ignore menus that are not in the menubar (PopUp and Toolbar) */ |
332 | if (parent != NULL || menu_is_menubar(menu->name)) { |
333 | if (menu->priority <= pri_tab[pri_idx]) { |
334 | lower_pri = menup; |
335 | } |
336 | } |
337 | menu = menu->next; |
338 | } |
339 | |
340 | if (menu == NULL) { |
341 | if (*next_name == NUL && parent == NULL) { |
342 | EMSG(_("E331: Must not add menu items directly to menu bar" )); |
343 | goto erret; |
344 | } |
345 | |
346 | if (menu_is_separator(dname) && *next_name != NUL) { |
347 | EMSG(_("E332: Separator cannot be part of a menu path" )); |
348 | goto erret; |
349 | } |
350 | |
351 | /* Not already there, so lets add it */ |
352 | menu = xcalloc(1, sizeof(vimmenu_T)); |
353 | |
354 | menu->modes = modes; |
355 | menu->enabled = MENU_ALL_MODES; |
356 | menu->name = vim_strsave(name); |
357 | // separate mnemonic and accelerator text from actual menu name |
358 | menu->dname = menu_text(name, &menu->mnemonic, &menu->actext); |
359 | if (en_name != NULL) { |
360 | menu->en_name = vim_strsave(en_name); |
361 | menu->en_dname = menu_text(en_name, NULL, NULL); |
362 | } else { |
363 | menu->en_name = NULL; |
364 | menu->en_dname = NULL; |
365 | } |
366 | menu->priority = pri_tab[pri_idx]; |
367 | menu->parent = parent; |
368 | |
369 | // Add after menu that has lower priority. |
370 | menu->next = *lower_pri; |
371 | *lower_pri = menu; |
372 | |
373 | old_modes = 0; |
374 | |
375 | } else { |
376 | old_modes = menu->modes; |
377 | |
378 | /* |
379 | * If this menu option was previously only available in other |
380 | * modes, then make sure it's available for this one now |
381 | * Also enable a menu when it's created or changed. |
382 | */ |
383 | { |
384 | menu->modes |= modes; |
385 | menu->enabled |= modes; |
386 | } |
387 | } |
388 | |
389 | |
390 | menup = &menu->children; |
391 | parent = menu; |
392 | name = next_name; |
393 | XFREE_CLEAR(dname); |
394 | if (pri_tab[pri_idx + 1] != -1) { |
395 | pri_idx++; |
396 | } |
397 | } |
398 | xfree(path_name); |
399 | |
400 | /* |
401 | * Only add system menu items which have not been defined yet. |
402 | * First check if this was an ":amenu". |
403 | */ |
404 | amenu = ((modes & (MENU_NORMAL_MODE | MENU_INSERT_MODE)) == |
405 | (MENU_NORMAL_MODE | MENU_INSERT_MODE)); |
406 | if (sys_menu) |
407 | modes &= ~old_modes; |
408 | |
409 | if (menu != NULL && modes) { |
410 | p = (call_data == NULL) ? NULL : vim_strsave(call_data); |
411 | |
412 | /* loop over all modes, may add more than one */ |
413 | for (i = 0; i < MENU_MODES; ++i) { |
414 | if (modes & (1 << i)) { |
415 | /* free any old menu */ |
416 | free_menu_string(menu, i); |
417 | |
418 | // For "amenu", may insert an extra character. |
419 | // Don't do this for "<Nop>". |
420 | c = 0; |
421 | d = 0; |
422 | if (amenu && call_data != NULL && *call_data != NUL) { |
423 | switch (1 << i) { |
424 | case MENU_VISUAL_MODE: |
425 | case MENU_SELECT_MODE: |
426 | case MENU_OP_PENDING_MODE: |
427 | case MENU_CMDLINE_MODE: |
428 | c = Ctrl_C; |
429 | break; |
430 | case MENU_INSERT_MODE: |
431 | c = Ctrl_BSL; |
432 | d = Ctrl_O; |
433 | break; |
434 | } |
435 | } |
436 | |
437 | if (c != 0) { |
438 | menu->strings[i] = xmalloc(STRLEN(call_data) + 5 ); |
439 | menu->strings[i][0] = c; |
440 | if (d == 0) { |
441 | STRCPY(menu->strings[i] + 1, call_data); |
442 | } else { |
443 | menu->strings[i][1] = d; |
444 | STRCPY(menu->strings[i] + 2, call_data); |
445 | } |
446 | if (c == Ctrl_C) { |
447 | int len = (int)STRLEN(menu->strings[i]); |
448 | |
449 | /* Append CTRL-\ CTRL-G to obey 'insertmode'. */ |
450 | menu->strings[i][len] = Ctrl_BSL; |
451 | menu->strings[i][len + 1] = Ctrl_G; |
452 | menu->strings[i][len + 2] = NUL; |
453 | } |
454 | } else { |
455 | menu->strings[i] = p; |
456 | } |
457 | menu->noremap[i] = menuarg->noremap[0]; |
458 | menu->silent[i] = menuarg->silent[0]; |
459 | } |
460 | } |
461 | } |
462 | return OK; |
463 | |
464 | erret: |
465 | xfree(path_name); |
466 | xfree(dname); |
467 | |
468 | /* Delete any empty submenu we added before discovering the error. Repeat |
469 | * for higher levels. */ |
470 | while (parent != NULL && parent->children == NULL) { |
471 | if (parent->parent == NULL) |
472 | menup = &root_menu; |
473 | else |
474 | menup = &parent->parent->children; |
475 | for (; *menup != NULL && *menup != parent; menup = &((*menup)->next)) |
476 | ; |
477 | if (*menup == NULL) /* safety check */ |
478 | break; |
479 | parent = parent->parent; |
480 | free_menu(menup); |
481 | } |
482 | return FAIL; |
483 | } |
484 | |
485 | /* |
486 | * Set the (sub)menu with the given name to enabled or disabled. |
487 | * Called recursively. |
488 | */ |
489 | static int (vimmenu_T *, |
490 | char_u *name, |
491 | int modes, |
492 | int enable) |
493 | { |
494 | char_u *p; |
495 | |
496 | if (menu == NULL) |
497 | return OK; /* Got to bottom of hierarchy */ |
498 | |
499 | /* Get name of this element in the menu hierarchy */ |
500 | p = menu_name_skip(name); |
501 | |
502 | /* Find the menu */ |
503 | while (menu != NULL) { |
504 | if (*name == NUL || *name == '*' || menu_name_equal(name, menu)) { |
505 | if (*p != NUL) { |
506 | if (menu->children == NULL) { |
507 | EMSG(_(e_notsubmenu)); |
508 | return FAIL; |
509 | } |
510 | if (menu_enable_recurse(menu->children, p, modes, enable) == FAIL) { |
511 | return FAIL; |
512 | } |
513 | } else if (enable) { |
514 | menu->enabled |= modes; |
515 | } else { |
516 | menu->enabled &= ~modes; |
517 | } |
518 | |
519 | /* |
520 | * When name is empty, we are doing all menu items for the given |
521 | * modes, so keep looping, otherwise we are just doing the named |
522 | * menu item (which has been found) so break here. |
523 | */ |
524 | if (*name != NUL && *name != '*') |
525 | break; |
526 | } |
527 | menu = menu->next; |
528 | } |
529 | if (*name != NUL && *name != '*' && menu == NULL) { |
530 | EMSG2(_(e_nomenu), name); |
531 | return FAIL; |
532 | } |
533 | |
534 | |
535 | return OK; |
536 | } |
537 | |
538 | /* |
539 | * Remove the (sub)menu with the given name from the menu hierarchy |
540 | * Called recursively. |
541 | */ |
542 | static int |
543 | ( |
544 | vimmenu_T **, |
545 | char_u *name, |
546 | int modes, |
547 | bool silent /* don't give error messages */ |
548 | ) |
549 | { |
550 | vimmenu_T *; |
551 | vimmenu_T *child; |
552 | char_u *p; |
553 | |
554 | if (*menup == NULL) |
555 | return OK; /* Got to bottom of hierarchy */ |
556 | |
557 | /* Get name of this element in the menu hierarchy */ |
558 | p = menu_name_skip(name); |
559 | |
560 | /* Find the menu */ |
561 | while ((menu = *menup) != NULL) { |
562 | if (*name == NUL || menu_name_equal(name, menu)) { |
563 | if (*p != NUL && menu->children == NULL) { |
564 | if (!silent) |
565 | EMSG(_(e_notsubmenu)); |
566 | return FAIL; |
567 | } |
568 | if ((menu->modes & modes) != 0x0) { |
569 | if (remove_menu(&menu->children, p, modes, silent) == FAIL) |
570 | return FAIL; |
571 | } else if (*name != NUL) { |
572 | if (!silent) |
573 | EMSG(_(e_othermode)); |
574 | return FAIL; |
575 | } |
576 | |
577 | /* |
578 | * When name is empty, we are removing all menu items for the given |
579 | * modes, so keep looping, otherwise we are just removing the named |
580 | * menu item (which has been found) so break here. |
581 | */ |
582 | if (*name != NUL) |
583 | break; |
584 | |
585 | /* Remove the menu item for the given mode[s]. If the menu item |
586 | * is no longer valid in ANY mode, delete it */ |
587 | menu->modes &= ~modes; |
588 | if (modes & MENU_TIP_MODE) |
589 | free_menu_string(menu, MENU_INDEX_TIP); |
590 | if ((menu->modes & MENU_ALL_MODES) == 0) |
591 | free_menu(menup); |
592 | else |
593 | menup = &menu->next; |
594 | } else |
595 | menup = &menu->next; |
596 | } |
597 | if (*name != NUL) { |
598 | if (menu == NULL) { |
599 | if (!silent) |
600 | EMSG2(_(e_nomenu), name); |
601 | return FAIL; |
602 | } |
603 | |
604 | |
605 | /* Recalculate modes for menu based on the new updated children */ |
606 | menu->modes &= ~modes; |
607 | child = menu->children; |
608 | for (; child != NULL; child = child->next) |
609 | menu->modes |= child->modes; |
610 | if (modes & MENU_TIP_MODE) { |
611 | free_menu_string(menu, MENU_INDEX_TIP); |
612 | } |
613 | if ((menu->modes & MENU_ALL_MODES) == 0) { |
614 | /* The menu item is no longer valid in ANY mode, so delete it */ |
615 | *menup = menu; |
616 | free_menu(menup); |
617 | } |
618 | } |
619 | |
620 | return OK; |
621 | } |
622 | |
623 | /* |
624 | * Free the given menu structure and remove it from the linked list. |
625 | */ |
626 | static void (vimmenu_T **) |
627 | { |
628 | int i; |
629 | vimmenu_T *; |
630 | |
631 | menu = *menup; |
632 | |
633 | |
634 | /* Don't change *menup until after calling gui_mch_destroy_menu(). The |
635 | * MacOS code needs the original structure to properly delete the menu. */ |
636 | *menup = menu->next; |
637 | xfree(menu->name); |
638 | xfree(menu->dname); |
639 | xfree(menu->en_name); |
640 | xfree(menu->en_dname); |
641 | xfree(menu->actext); |
642 | for (i = 0; i < MENU_MODES; i++) |
643 | free_menu_string(menu, i); |
644 | xfree(menu); |
645 | |
646 | } |
647 | |
648 | /* |
649 | * Free the menu->string with the given index. |
650 | */ |
651 | static void (vimmenu_T *, int idx) |
652 | { |
653 | int count = 0; |
654 | int i; |
655 | |
656 | for (i = 0; i < MENU_MODES; i++) |
657 | if (menu->strings[i] == menu->strings[idx]) |
658 | count++; |
659 | if (count == 1) |
660 | xfree(menu->strings[idx]); |
661 | menu->strings[idx] = NULL; |
662 | } |
663 | |
664 | /// Export menus |
665 | /// |
666 | /// @param[in] menu if null, starts from root_menu |
667 | /// @param modes, a choice of \ref MENU_MODES |
668 | /// @return dict with name/commands |
669 | /// @see show_menus_recursive |
670 | /// @see menu_get |
671 | static dict_T *(const vimmenu_T *, int modes) |
672 | { |
673 | dict_T *dict; |
674 | |
675 | if (!menu || (menu->modes & modes) == 0x0) { |
676 | return NULL; |
677 | } |
678 | |
679 | dict = tv_dict_alloc(); |
680 | tv_dict_add_str(dict, S_LEN("name" ), (char *)menu->dname); |
681 | tv_dict_add_nr(dict, S_LEN("priority" ), (int)menu->priority); |
682 | tv_dict_add_nr(dict, S_LEN("hidden" ), menu_is_hidden(menu->dname)); |
683 | |
684 | if (menu->mnemonic) { |
685 | char buf[MB_MAXCHAR + 1] = { 0 }; // > max value of utf8_char2bytes |
686 | utf_char2bytes(menu->mnemonic, (char_u *)buf); |
687 | tv_dict_add_str(dict, S_LEN("shortcut" ), buf); |
688 | } |
689 | |
690 | if (menu->actext) { |
691 | tv_dict_add_str(dict, S_LEN("actext" ), (char *)menu->actext); |
692 | } |
693 | |
694 | if (menu->modes & MENU_TIP_MODE && menu->strings[MENU_INDEX_TIP]) { |
695 | tv_dict_add_str(dict, S_LEN("tooltip" ), |
696 | (char *)menu->strings[MENU_INDEX_TIP]); |
697 | } |
698 | |
699 | if (!menu->children) { |
700 | // leaf menu |
701 | dict_T *commands = tv_dict_alloc(); |
702 | tv_dict_add_dict(dict, S_LEN("mappings" ), commands); |
703 | |
704 | for (int bit = 0; bit < MENU_MODES; bit++) { |
705 | if ((menu->modes & modes & (1 << bit)) != 0) { |
706 | dict_T *impl = tv_dict_alloc(); |
707 | tv_dict_add_allocated_str(impl, S_LEN("rhs" ), |
708 | str2special_save((char *)menu->strings[bit], |
709 | false, false)); |
710 | tv_dict_add_nr(impl, S_LEN("silent" ), menu->silent[bit]); |
711 | tv_dict_add_nr(impl, S_LEN("enabled" ), |
712 | (menu->enabled & (1 << bit)) ? 1 : 0); |
713 | tv_dict_add_nr(impl, S_LEN("noremap" ), |
714 | (menu->noremap[bit] & REMAP_NONE) ? 1 : 0); |
715 | tv_dict_add_nr(impl, S_LEN("sid" ), |
716 | (menu->noremap[bit] & REMAP_SCRIPT) ? 1 : 0); |
717 | tv_dict_add_dict(commands, (char *)&menu_mode_chars[bit], 1, impl); |
718 | } |
719 | } |
720 | } else { |
721 | // visit recursively all children |
722 | list_T *const children_list = tv_list_alloc(kListLenMayKnow); |
723 | for (menu = menu->children; menu != NULL; menu = menu->next) { |
724 | dict_T *d = menu_get_recursive(menu, modes); |
725 | if (tv_dict_len(d) > 0) { |
726 | tv_list_append_dict(children_list, d); |
727 | } |
728 | } |
729 | tv_dict_add_list(dict, S_LEN("submenus" ), children_list); |
730 | } |
731 | return dict; |
732 | } |
733 | |
734 | |
735 | /// Export menus matching path \p path_name |
736 | /// |
737 | /// @param path_name |
738 | /// @param modes supported modes, see \ref MENU_MODES |
739 | /// @param[in,out] list must be allocated |
740 | /// @return false if could not find path_name |
741 | bool (char_u *const path_name, int modes, list_T *list) |
742 | { |
743 | vimmenu_T * = find_menu(root_menu, path_name, modes); |
744 | if (!menu) { |
745 | return false; |
746 | } |
747 | for (; menu != NULL; menu = menu->next) { |
748 | dict_T *d = menu_get_recursive(menu, modes); |
749 | if (d && tv_dict_len(d) > 0) { |
750 | tv_list_append_dict(list, d); |
751 | } |
752 | if (*path_name != NUL) { |
753 | // If a (non-empty) path query was given, only the first node in the |
754 | // find_menu() result is relevant. Else we want all nodes. |
755 | break; |
756 | } |
757 | } |
758 | return true; |
759 | } |
760 | |
761 | |
762 | /// Find menu matching `name` and `modes`. |
763 | /// |
764 | /// @param menu top menu to start looking from |
765 | /// @param name path towards the menu |
766 | /// @return menu if \p name is null, found menu or NULL |
767 | static vimmenu_T *(vimmenu_T *, char_u *name, int modes) |
768 | { |
769 | char_u *p; |
770 | |
771 | while (*name) { |
772 | // find the end of one dot-separated name and put a NUL at the dot |
773 | p = menu_name_skip(name); |
774 | while (menu != NULL) { |
775 | if (menu_name_equal(name, menu)) { |
776 | // Found menu |
777 | if (*p != NUL && menu->children == NULL) { |
778 | EMSG(_(e_notsubmenu)); |
779 | return NULL; |
780 | } else if ((menu->modes & modes) == 0x0) { |
781 | EMSG(_(e_othermode)); |
782 | return NULL; |
783 | } else if (*p == NUL) { // found a full match |
784 | return menu; |
785 | } |
786 | break; |
787 | } |
788 | menu = menu->next; |
789 | } |
790 | |
791 | if (menu == NULL) { |
792 | EMSG2(_(e_nomenu), name); |
793 | return NULL; |
794 | } |
795 | // Found a match, search the sub-menu. |
796 | name = p; |
797 | menu = menu->children; |
798 | } |
799 | return menu; |
800 | } |
801 | |
802 | /// Show the mapping associated with a menu item or hierarchy in a sub-menu. |
803 | static int (char_u *const path_name, int modes) |
804 | { |
805 | vimmenu_T *; |
806 | |
807 | // First, find the (sub)menu with the given name |
808 | menu = find_menu(root_menu, path_name, modes); |
809 | if (!menu) { |
810 | return FAIL; |
811 | } |
812 | |
813 | /* Now we have found the matching menu, and we list the mappings */ |
814 | /* Highlight title */ |
815 | MSG_PUTS_TITLE(_("\n--- Menus ---" )); |
816 | |
817 | show_menus_recursive(menu->parent, modes, 0); |
818 | return OK; |
819 | } |
820 | |
821 | /// Recursively show the mappings associated with the menus under the given one |
822 | static void (vimmenu_T *, int modes, int depth) |
823 | { |
824 | int i; |
825 | int bit; |
826 | |
827 | if (menu != NULL && (menu->modes & modes) == 0x0) |
828 | return; |
829 | |
830 | if (menu != NULL) { |
831 | msg_putchar('\n'); |
832 | if (got_int) /* "q" hit for "--more--" */ |
833 | return; |
834 | for (i = 0; i < depth; i++) |
835 | MSG_PUTS(" " ); |
836 | if (menu->priority) { |
837 | msg_outnum((long)menu->priority); |
838 | MSG_PUTS(" " ); |
839 | } |
840 | // Same highlighting as for directories!? |
841 | msg_outtrans_attr(menu->name, HL_ATTR(HLF_D)); |
842 | } |
843 | |
844 | if (menu != NULL && menu->children == NULL) { |
845 | for (bit = 0; bit < MENU_MODES; bit++) |
846 | if ((menu->modes & modes & (1 << bit)) != 0) { |
847 | msg_putchar('\n'); |
848 | if (got_int) /* "q" hit for "--more--" */ |
849 | return; |
850 | for (i = 0; i < depth + 2; i++) |
851 | MSG_PUTS(" " ); |
852 | msg_putchar(menu_mode_chars[bit]); |
853 | if (menu->noremap[bit] == REMAP_NONE) |
854 | msg_putchar('*'); |
855 | else if (menu->noremap[bit] == REMAP_SCRIPT) |
856 | msg_putchar('&'); |
857 | else |
858 | msg_putchar(' '); |
859 | if (menu->silent[bit]) |
860 | msg_putchar('s'); |
861 | else |
862 | msg_putchar(' '); |
863 | if ((menu->modes & menu->enabled & (1 << bit)) == 0) |
864 | msg_putchar('-'); |
865 | else |
866 | msg_putchar(' '); |
867 | MSG_PUTS(" " ); |
868 | if (*menu->strings[bit] == NUL) { |
869 | msg_puts_attr("<Nop>" , HL_ATTR(HLF_8)); |
870 | } else { |
871 | msg_outtrans_special(menu->strings[bit], false); |
872 | } |
873 | } |
874 | } else { |
875 | if (menu == NULL) { |
876 | menu = root_menu; |
877 | depth--; |
878 | } else |
879 | menu = menu->children; |
880 | |
881 | /* recursively show all children. Skip PopUp[nvoci]. */ |
882 | for (; menu != NULL && !got_int; menu = menu->next) |
883 | if (!menu_is_hidden(menu->dname)) |
884 | show_menus_recursive(menu, modes, depth + 1); |
885 | } |
886 | } |
887 | |
888 | |
889 | /* |
890 | * Used when expanding menu names. |
891 | */ |
892 | static vimmenu_T *expand_menu = NULL; |
893 | static int expand_modes = 0x0; |
894 | static int expand_emenu; /* TRUE for ":emenu" command */ |
895 | |
896 | /* |
897 | * Work out what to complete when doing command line completion of menu names. |
898 | */ |
899 | char_u *(expand_T *xp, char_u *cmd, char_u *arg, int forceit) |
900 | { |
901 | char_u *after_dot; |
902 | char_u *p; |
903 | char_u *path_name = NULL; |
904 | char_u *name; |
905 | int ; |
906 | vimmenu_T *; |
907 | int expand_menus; |
908 | |
909 | xp->xp_context = EXPAND_UNSUCCESSFUL; |
910 | |
911 | |
912 | /* Check for priority numbers, enable and disable */ |
913 | for (p = arg; *p; ++p) |
914 | if (!ascii_isdigit(*p) && *p != '.') |
915 | break; |
916 | |
917 | if (!ascii_iswhite(*p)) { |
918 | if (STRNCMP(arg, "enable" , 6) == 0 |
919 | && (arg[6] == NUL || ascii_iswhite(arg[6]))) |
920 | p = arg + 6; |
921 | else if (STRNCMP(arg, "disable" , 7) == 0 |
922 | && (arg[7] == NUL || ascii_iswhite(arg[7]))) |
923 | p = arg + 7; |
924 | else |
925 | p = arg; |
926 | } |
927 | |
928 | while (*p != NUL && ascii_iswhite(*p)) |
929 | ++p; |
930 | |
931 | arg = after_dot = p; |
932 | |
933 | for (; *p && !ascii_iswhite(*p); ++p) { |
934 | if ((*p == '\\' || *p == Ctrl_V) && p[1] != NUL) |
935 | p++; |
936 | else if (*p == '.') |
937 | after_dot = p + 1; |
938 | } |
939 | |
940 | // ":popup" only uses menues, not entries |
941 | expand_menus = !((*cmd == 't' && cmd[1] == 'e') || *cmd == 'p'); |
942 | expand_emenu = (*cmd == 'e'); |
943 | if (expand_menus && ascii_iswhite(*p)) |
944 | return NULL; /* TODO: check for next command? */ |
945 | if (*p == NUL) { /* Complete the menu name */ |
946 | /* |
947 | * With :unmenu, you only want to match menus for the appropriate mode. |
948 | * With :menu though you might want to add a menu with the same name as |
949 | * one in another mode, so match menus from other modes too. |
950 | */ |
951 | expand_modes = get_menu_cmd_modes(cmd, forceit, NULL, &unmenu); |
952 | if (!unmenu) |
953 | expand_modes = MENU_ALL_MODES; |
954 | |
955 | menu = root_menu; |
956 | if (after_dot > arg) { |
957 | size_t path_len = (size_t) (after_dot - arg); |
958 | path_name = xmalloc(path_len); |
959 | STRLCPY(path_name, arg, path_len); |
960 | } |
961 | name = path_name; |
962 | while (name != NULL && *name) { |
963 | p = menu_name_skip(name); |
964 | while (menu != NULL) { |
965 | if (menu_name_equal(name, menu)) { |
966 | /* Found menu */ |
967 | if ((*p != NUL && menu->children == NULL) |
968 | || ((menu->modes & expand_modes) == 0x0)) { |
969 | /* |
970 | * Menu path continues, but we have reached a leaf. |
971 | * Or menu exists only in another mode. |
972 | */ |
973 | xfree(path_name); |
974 | return NULL; |
975 | } |
976 | break; |
977 | } |
978 | menu = menu->next; |
979 | } |
980 | if (menu == NULL) { |
981 | /* No menu found with the name we were looking for */ |
982 | xfree(path_name); |
983 | return NULL; |
984 | } |
985 | name = p; |
986 | menu = menu->children; |
987 | } |
988 | xfree(path_name); |
989 | |
990 | xp->xp_context = expand_menus ? EXPAND_MENUNAMES : EXPAND_MENUS; |
991 | xp->xp_pattern = after_dot; |
992 | expand_menu = menu; |
993 | } else /* We're in the mapping part */ |
994 | xp->xp_context = EXPAND_NOTHING; |
995 | return NULL; |
996 | } |
997 | |
998 | /* |
999 | * Function given to ExpandGeneric() to obtain the list of (sub)menus (not |
1000 | * entries). |
1001 | */ |
1002 | char_u *(expand_T *xp, int idx) |
1003 | { |
1004 | static vimmenu_T * = NULL; |
1005 | char_u *str; |
1006 | static int should_advance = FALSE; |
1007 | |
1008 | if (idx == 0) { /* first call: start at first item */ |
1009 | menu = expand_menu; |
1010 | should_advance = FALSE; |
1011 | } |
1012 | |
1013 | /* Skip PopUp[nvoci]. */ |
1014 | while (menu != NULL && (menu_is_hidden(menu->dname) |
1015 | || menu_is_separator(menu->dname) |
1016 | || menu->children == NULL)) |
1017 | menu = menu->next; |
1018 | |
1019 | if (menu == NULL) /* at end of linked list */ |
1020 | return NULL; |
1021 | |
1022 | if (menu->modes & expand_modes) |
1023 | if (should_advance) |
1024 | str = menu->en_dname; |
1025 | else { |
1026 | str = menu->dname; |
1027 | if (menu->en_dname == NULL) |
1028 | should_advance = TRUE; |
1029 | } |
1030 | else |
1031 | str = (char_u *)"" ; |
1032 | |
1033 | if (should_advance) |
1034 | /* Advance to next menu entry. */ |
1035 | menu = menu->next; |
1036 | |
1037 | should_advance = !should_advance; |
1038 | |
1039 | return str; |
1040 | } |
1041 | |
1042 | /* |
1043 | * Function given to ExpandGeneric() to obtain the list of menus and menu |
1044 | * entries. |
1045 | */ |
1046 | char_u *(expand_T *xp, int idx) |
1047 | { |
1048 | static vimmenu_T * = NULL; |
1049 | #define TBUFFER_LEN 256 |
1050 | static char_u tbuffer[TBUFFER_LEN]; /*hack*/ |
1051 | char_u *str; |
1052 | static int should_advance = FALSE; |
1053 | |
1054 | if (idx == 0) { /* first call: start at first item */ |
1055 | menu = expand_menu; |
1056 | should_advance = FALSE; |
1057 | } |
1058 | |
1059 | /* Skip Browse-style entries, popup menus and separators. */ |
1060 | while (menu != NULL |
1061 | && ( menu_is_hidden(menu->dname) |
1062 | || (expand_emenu && menu_is_separator(menu->dname)) |
1063 | || menu->dname[STRLEN(menu->dname) - 1] == '.' |
1064 | )) |
1065 | menu = menu->next; |
1066 | |
1067 | if (menu == NULL) /* at end of linked list */ |
1068 | return NULL; |
1069 | |
1070 | if (menu->modes & expand_modes) { |
1071 | if (menu->children != NULL) { |
1072 | if (should_advance) |
1073 | STRLCPY(tbuffer, menu->en_dname, TBUFFER_LEN - 1); |
1074 | else { |
1075 | STRLCPY(tbuffer, menu->dname, TBUFFER_LEN - 1); |
1076 | if (menu->en_dname == NULL) |
1077 | should_advance = TRUE; |
1078 | } |
1079 | /* hack on menu separators: use a 'magic' char for the separator |
1080 | * so that '.' in names gets escaped properly */ |
1081 | STRCAT(tbuffer, "\001" ); |
1082 | str = tbuffer; |
1083 | } else { |
1084 | if (should_advance) |
1085 | str = menu->en_dname; |
1086 | else { |
1087 | str = menu->dname; |
1088 | if (menu->en_dname == NULL) |
1089 | should_advance = TRUE; |
1090 | } |
1091 | } |
1092 | } else |
1093 | str = (char_u *)"" ; |
1094 | |
1095 | if (should_advance) |
1096 | /* Advance to next menu entry. */ |
1097 | menu = menu->next; |
1098 | |
1099 | should_advance = !should_advance; |
1100 | |
1101 | return str; |
1102 | } |
1103 | |
1104 | |
1105 | /// Skip over this element of the menu path and return the start of the next |
1106 | /// element. Any \ and ^Vs are removed from the current element. |
1107 | /// |
1108 | /// @param name may be modified. |
1109 | /// @return start of the next element |
1110 | char_u *(char_u *const name) |
1111 | { |
1112 | char_u *p; |
1113 | |
1114 | for (p = name; *p && *p != '.'; MB_PTR_ADV(p)) { |
1115 | if (*p == '\\' || *p == Ctrl_V) { |
1116 | STRMOVE(p, p + 1); |
1117 | if (*p == NUL) |
1118 | break; |
1119 | } |
1120 | } |
1121 | if (*p) |
1122 | *p++ = NUL; |
1123 | return p; |
1124 | } |
1125 | |
1126 | /* |
1127 | * Return TRUE when "name" matches with menu "menu". The name is compared in |
1128 | * two ways: raw menu name and menu name without '&'. ignore part after a TAB. |
1129 | */ |
1130 | static bool (const char_u *const name, vimmenu_T *const ) |
1131 | { |
1132 | if (menu->en_name != NULL |
1133 | && (menu_namecmp(name, menu->en_name) |
1134 | || menu_namecmp(name, menu->en_dname))) |
1135 | return true; |
1136 | return menu_namecmp(name, menu->name) || menu_namecmp(name, menu->dname); |
1137 | } |
1138 | |
1139 | static bool (const char_u *const name, const char_u *const mname) |
1140 | { |
1141 | int i; |
1142 | |
1143 | for (i = 0; name[i] != NUL && name[i] != TAB; ++i) |
1144 | if (name[i] != mname[i]) |
1145 | break; |
1146 | return (name[i] == NUL || name[i] == TAB) |
1147 | && (mname[i] == NUL || mname[i] == TAB); |
1148 | } |
1149 | |
1150 | |
1151 | /// Returns the \ref MENU_MODES specified by menu command `cmd`. |
1152 | /// (eg :menu! returns MENU_CMDLINE_MODE | MENU_INSERT_MODE) |
1153 | /// |
1154 | /// @param[in] cmd string like "nmenu", "vmenu", etc. |
1155 | /// @param[in] forceit bang (!) was given after the command |
1156 | /// @param[out] noremap If not NULL, the flag it points to is set according |
1157 | /// to whether the command is a "nore" command. |
1158 | /// @param[out] unmenu If not NULL, the flag it points to is set according |
1159 | /// to whether the command is an "unmenu" command. |
1160 | int |
1161 | ( |
1162 | const char_u * cmd, |
1163 | bool forceit, |
1164 | int *noremap, |
1165 | int * |
1166 | ) |
1167 | { |
1168 | int modes; |
1169 | |
1170 | switch (*cmd++) { |
1171 | case 'v': /* vmenu, vunmenu, vnoremenu */ |
1172 | modes = MENU_VISUAL_MODE | MENU_SELECT_MODE; |
1173 | break; |
1174 | case 'x': /* xmenu, xunmenu, xnoremenu */ |
1175 | modes = MENU_VISUAL_MODE; |
1176 | break; |
1177 | case 's': /* smenu, sunmenu, snoremenu */ |
1178 | modes = MENU_SELECT_MODE; |
1179 | break; |
1180 | case 'o': /* omenu */ |
1181 | modes = MENU_OP_PENDING_MODE; |
1182 | break; |
1183 | case 'i': /* imenu */ |
1184 | modes = MENU_INSERT_MODE; |
1185 | break; |
1186 | case 't': |
1187 | modes = MENU_TIP_MODE; /* tmenu */ |
1188 | break; |
1189 | case 'c': /* cmenu */ |
1190 | modes = MENU_CMDLINE_MODE; |
1191 | break; |
1192 | case 'a': /* amenu */ |
1193 | modes = MENU_INSERT_MODE | MENU_CMDLINE_MODE | MENU_NORMAL_MODE |
1194 | | MENU_VISUAL_MODE | MENU_SELECT_MODE |
1195 | | MENU_OP_PENDING_MODE; |
1196 | break; |
1197 | case 'n': |
1198 | if (*cmd != 'o') { /* nmenu, not noremenu */ |
1199 | modes = MENU_NORMAL_MODE; |
1200 | break; |
1201 | } |
1202 | FALLTHROUGH; |
1203 | default: |
1204 | cmd--; |
1205 | if (forceit) { |
1206 | // menu!! |
1207 | modes = MENU_INSERT_MODE | MENU_CMDLINE_MODE; |
1208 | } else { |
1209 | // menu |
1210 | modes = MENU_NORMAL_MODE | MENU_VISUAL_MODE | MENU_SELECT_MODE |
1211 | | MENU_OP_PENDING_MODE; |
1212 | } |
1213 | } |
1214 | |
1215 | if (noremap != NULL) |
1216 | *noremap = (*cmd == 'n' ? REMAP_NONE : REMAP_YES); |
1217 | if (unmenu != NULL) |
1218 | *unmenu = (*cmd == 'u'); |
1219 | return modes; |
1220 | } |
1221 | |
1222 | /* |
1223 | * Modify a menu name starting with "PopUp" to include the mode character. |
1224 | * Returns the name in allocated memory. |
1225 | */ |
1226 | static char_u *(char_u *name, int idx) |
1227 | { |
1228 | size_t len = STRLEN(name); |
1229 | assert(len >= 4); |
1230 | |
1231 | char_u *p = vim_strnsave(name, len + 1); |
1232 | memmove(p + 6, p + 5, len - 4); |
1233 | p[5] = menu_mode_chars[idx]; |
1234 | |
1235 | return p; |
1236 | } |
1237 | |
1238 | |
1239 | /// Duplicate the menu item text and then process to see if a mnemonic key |
1240 | /// and/or accelerator text has been identified. |
1241 | /// |
1242 | /// @param str The menu item text. |
1243 | /// @param[out] mnemonic If non-NULL, *mnemonic is set to the character after |
1244 | /// the first '&'. |
1245 | /// @param[out] actext If non-NULL, *actext is set to the text after the first |
1246 | /// TAB, but only if a TAB was found. Memory pointed to is newly |
1247 | /// allocated. |
1248 | /// |
1249 | /// @return a pointer to allocated memory. |
1250 | static char_u *(const char_u *str, int *mnemonic, char_u **actext) |
1251 | FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT |
1252 | FUNC_ATTR_NONNULL_ARG(1) |
1253 | { |
1254 | char_u *p; |
1255 | char_u *text; |
1256 | |
1257 | /* Locate accelerator text, after the first TAB */ |
1258 | p = vim_strchr(str, TAB); |
1259 | if (p != NULL) { |
1260 | if (actext != NULL) |
1261 | *actext = vim_strsave(p + 1); |
1262 | assert(p >= str); |
1263 | text = vim_strnsave(str, (size_t)(p - str)); |
1264 | } else |
1265 | text = vim_strsave(str); |
1266 | |
1267 | /* Find mnemonic characters "&a" and reduce "&&" to "&". */ |
1268 | for (p = text; p != NULL; ) { |
1269 | p = vim_strchr(p, '&'); |
1270 | if (p != NULL) { |
1271 | if (p[1] == NUL) /* trailing "&" */ |
1272 | break; |
1273 | if (mnemonic != NULL && p[1] != '&') |
1274 | *mnemonic = p[1]; |
1275 | STRMOVE(p, p + 1); |
1276 | p = p + 1; |
1277 | } |
1278 | } |
1279 | return text; |
1280 | } |
1281 | |
1282 | /* |
1283 | * Return TRUE if "name" can be a menu in the MenuBar. |
1284 | */ |
1285 | int (char_u *name) |
1286 | { |
1287 | return !menu_is_popup(name) |
1288 | && !menu_is_toolbar(name) |
1289 | && *name != MNU_HIDDEN_CHAR; |
1290 | } |
1291 | |
1292 | /* |
1293 | * Return TRUE if "name" is a popup menu name. |
1294 | */ |
1295 | int (char_u *name) |
1296 | { |
1297 | return STRNCMP(name, "PopUp" , 5) == 0; |
1298 | } |
1299 | |
1300 | |
1301 | /* |
1302 | * Return TRUE if "name" is a toolbar menu name. |
1303 | */ |
1304 | int (char_u *name) |
1305 | { |
1306 | return STRNCMP(name, "ToolBar" , 7) == 0; |
1307 | } |
1308 | |
1309 | /* |
1310 | * Return TRUE if the name is a menu separator identifier: Starts and ends |
1311 | * with '-' |
1312 | */ |
1313 | int (char_u *name) |
1314 | { |
1315 | return name[0] == '-' && name[STRLEN(name) - 1] == '-'; |
1316 | } |
1317 | |
1318 | |
1319 | /// True if a popup menu or starts with \ref MNU_HIDDEN_CHAR |
1320 | /// |
1321 | /// @return true if the menu is hidden |
1322 | static int (char_u *name) |
1323 | { |
1324 | return (name[0] == MNU_HIDDEN_CHAR) |
1325 | || (menu_is_popup(name) && name[5] != NUL); |
1326 | } |
1327 | |
1328 | /* |
1329 | * Given a menu descriptor, e.g. "File.New", find it in the menu hierarchy and |
1330 | * execute it. |
1331 | */ |
1332 | void (exarg_T *eap) |
1333 | { |
1334 | vimmenu_T *; |
1335 | char_u *name; |
1336 | char_u *saved_name; |
1337 | char_u *p; |
1338 | int idx; |
1339 | char_u *mode; |
1340 | |
1341 | saved_name = vim_strsave(eap->arg); |
1342 | |
1343 | menu = root_menu; |
1344 | name = saved_name; |
1345 | while (*name) { |
1346 | /* Find in the menu hierarchy */ |
1347 | p = menu_name_skip(name); |
1348 | |
1349 | while (menu != NULL) { |
1350 | if (menu_name_equal(name, menu)) { |
1351 | if (*p == NUL && menu->children != NULL) { |
1352 | EMSG(_("E333: Menu path must lead to a menu item" )); |
1353 | menu = NULL; |
1354 | } else if (*p != NUL && menu->children == NULL) { |
1355 | EMSG(_(e_notsubmenu)); |
1356 | menu = NULL; |
1357 | } |
1358 | break; |
1359 | } |
1360 | menu = menu->next; |
1361 | } |
1362 | if (menu == NULL || *p == NUL) |
1363 | break; |
1364 | menu = menu->children; |
1365 | name = p; |
1366 | } |
1367 | xfree(saved_name); |
1368 | if (menu == NULL) { |
1369 | EMSG2(_("E334: Menu not found: %s" ), eap->arg); |
1370 | return; |
1371 | } |
1372 | |
1373 | /* Found the menu, so execute. |
1374 | * Use the Insert mode entry when returning to Insert mode. */ |
1375 | if (((State & INSERT) || restart_edit) && !current_sctx.sc_sid) { |
1376 | mode = (char_u *)"Insert" ; |
1377 | idx = MENU_INDEX_INSERT; |
1378 | } else if (State & CMDLINE) { |
1379 | mode = (char_u *)"Command" ; |
1380 | idx = MENU_INDEX_CMDLINE; |
1381 | } else if (get_real_state() & VISUAL) { |
1382 | /* Detect real visual mode -- if we are really in visual mode we |
1383 | * don't need to do any guesswork to figure out what the selection |
1384 | * is. Just execute the visual binding for the menu. */ |
1385 | mode = (char_u *)"Visual" ; |
1386 | idx = MENU_INDEX_VISUAL; |
1387 | } else if (eap->addr_count) { |
1388 | pos_T tpos; |
1389 | |
1390 | mode = (char_u *)"Visual" ; |
1391 | idx = MENU_INDEX_VISUAL; |
1392 | |
1393 | /* GEDDES: This is not perfect - but it is a |
1394 | * quick way of detecting whether we are doing this from a |
1395 | * selection - see if the range matches up with the visual |
1396 | * select start and end. */ |
1397 | if ((curbuf->b_visual.vi_start.lnum == eap->line1) |
1398 | && (curbuf->b_visual.vi_end.lnum) == eap->line2) { |
1399 | /* Set it up for visual mode - equivalent to gv. */ |
1400 | VIsual_mode = curbuf->b_visual.vi_mode; |
1401 | tpos = curbuf->b_visual.vi_end; |
1402 | curwin->w_cursor = curbuf->b_visual.vi_start; |
1403 | curwin->w_curswant = curbuf->b_visual.vi_curswant; |
1404 | } else { |
1405 | /* Set it up for line-wise visual mode */ |
1406 | VIsual_mode = 'V'; |
1407 | curwin->w_cursor.lnum = eap->line1; |
1408 | curwin->w_cursor.col = 1; |
1409 | tpos.lnum = eap->line2; |
1410 | tpos.col = MAXCOL; |
1411 | tpos.coladd = 0; |
1412 | } |
1413 | |
1414 | /* Activate visual mode */ |
1415 | VIsual_active = TRUE; |
1416 | VIsual_reselect = TRUE; |
1417 | check_cursor(); |
1418 | VIsual = curwin->w_cursor; |
1419 | curwin->w_cursor = tpos; |
1420 | |
1421 | check_cursor(); |
1422 | |
1423 | /* Adjust the cursor to make sure it is in the correct pos |
1424 | * for exclusive mode */ |
1425 | if (*p_sel == 'e' && gchar_cursor() != NUL) |
1426 | ++curwin->w_cursor.col; |
1427 | } else { |
1428 | mode = (char_u *)"Normal" ; |
1429 | idx = MENU_INDEX_NORMAL; |
1430 | } |
1431 | |
1432 | assert(idx != MENU_INDEX_INVALID); |
1433 | if (menu->strings[idx] != NULL) { |
1434 | // When executing a script or function execute the commands right now. |
1435 | // Otherwise put them in the typeahead buffer. |
1436 | if (current_sctx.sc_sid != 0) { |
1437 | exec_normal_cmd(menu->strings[idx], menu->noremap[idx], |
1438 | menu->silent[idx]); |
1439 | } else { |
1440 | ins_typebuf(menu->strings[idx], menu->noremap[idx], 0, true, |
1441 | menu->silent[idx]); |
1442 | } |
1443 | } else { |
1444 | EMSG2(_("E335: Menu not defined for %s mode" ), mode); |
1445 | } |
1446 | } |
1447 | |
1448 | /* |
1449 | * Translation of menu names. Just a simple lookup table. |
1450 | */ |
1451 | |
1452 | typedef struct { |
1453 | char_u *from; /* English name */ |
1454 | char_u *from_noamp; /* same, without '&' */ |
1455 | char_u *to; /* translated name */ |
1456 | } ; |
1457 | |
1458 | static garray_T = GA_EMPTY_INIT_VALUE; |
1459 | |
1460 | #define (mt) \ |
1461 | menutrans_T* _mt = (mt); \ |
1462 | xfree(_mt->from); \ |
1463 | xfree(_mt->from_noamp); \ |
1464 | xfree(_mt->to) |
1465 | |
1466 | /* |
1467 | * ":menutrans". |
1468 | * This function is also defined without the +multi_lang feature, in which |
1469 | * case the commands are ignored. |
1470 | */ |
1471 | void (exarg_T *eap) |
1472 | { |
1473 | char_u *arg = eap->arg; |
1474 | char_u *from, *from_noamp, *to; |
1475 | |
1476 | if (menutrans_ga.ga_itemsize == 0) |
1477 | ga_init(&menutrans_ga, (int)sizeof(menutrans_T), 5); |
1478 | |
1479 | /* |
1480 | * ":menutrans clear": clear all translations. |
1481 | */ |
1482 | if (STRNCMP(arg, "clear" , 5) == 0 && ends_excmd(*skipwhite(arg + 5))) { |
1483 | GA_DEEP_CLEAR(&menutrans_ga, menutrans_T, FREE_MENUTRANS); |
1484 | |
1485 | /* Delete all "menutrans_" global variables. */ |
1486 | del_menutrans_vars(); |
1487 | } else { |
1488 | /* ":menutrans from to": add translation */ |
1489 | from = arg; |
1490 | arg = menu_skip_part(arg); |
1491 | to = skipwhite(arg); |
1492 | *arg = NUL; |
1493 | arg = menu_skip_part(to); |
1494 | if (arg == to) |
1495 | EMSG(_(e_invarg)); |
1496 | else { |
1497 | from = vim_strsave(from); |
1498 | from_noamp = menu_text(from, NULL, NULL); |
1499 | assert(arg >= to); |
1500 | to = vim_strnsave(to, (size_t)(arg - to)); |
1501 | menu_translate_tab_and_shift(from); |
1502 | menu_translate_tab_and_shift(to); |
1503 | menu_unescape_name(from); |
1504 | menu_unescape_name(to); |
1505 | menutrans_T* tp = GA_APPEND_VIA_PTR(menutrans_T, &menutrans_ga); |
1506 | tp->from = from; |
1507 | tp->from_noamp = from_noamp; |
1508 | tp->to = to; |
1509 | } |
1510 | } |
1511 | } |
1512 | |
1513 | /* |
1514 | * Find the character just after one part of a menu name. |
1515 | */ |
1516 | static char_u *(char_u *p) |
1517 | { |
1518 | while (*p != NUL && *p != '.' && !ascii_iswhite(*p)) { |
1519 | if ((*p == '\\' || *p == Ctrl_V) && p[1] != NUL) |
1520 | ++p; |
1521 | ++p; |
1522 | } |
1523 | return p; |
1524 | } |
1525 | |
1526 | /* |
1527 | * Lookup part of a menu name in the translations. |
1528 | * Return a pointer to the translation or NULL if not found. |
1529 | */ |
1530 | static char_u *(char_u *name, int len) |
1531 | { |
1532 | menutrans_T *tp = (menutrans_T *)menutrans_ga.ga_data; |
1533 | char_u *dname; |
1534 | |
1535 | for (int i = 0; i < menutrans_ga.ga_len; i++) { |
1536 | if (STRNICMP(name, tp[i].from, len) == 0 && tp[i].from[len] == NUL) { |
1537 | return tp[i].to; |
1538 | } |
1539 | } |
1540 | |
1541 | /* Now try again while ignoring '&' characters. */ |
1542 | char_u c = name[len]; |
1543 | name[len] = NUL; |
1544 | dname = menu_text(name, NULL, NULL); |
1545 | name[len] = c; |
1546 | for (int i = 0; i < menutrans_ga.ga_len; i++) { |
1547 | if (STRICMP(dname, tp[i].from_noamp) == 0) { |
1548 | xfree(dname); |
1549 | return tp[i].to; |
1550 | } |
1551 | } |
1552 | xfree(dname); |
1553 | |
1554 | return NULL; |
1555 | } |
1556 | |
1557 | /* |
1558 | * Unescape the name in the translate dictionary table. |
1559 | */ |
1560 | static void (char_u *name) |
1561 | { |
1562 | char_u *p; |
1563 | |
1564 | for (p = name; *p && *p != '.'; MB_PTR_ADV(p)) { |
1565 | if (*p == '\\') { |
1566 | STRMOVE(p, p + 1); |
1567 | } |
1568 | } |
1569 | } |
1570 | |
1571 | /* |
1572 | * Isolate the menu name. |
1573 | * Skip the menu name, and translate <Tab> into a real TAB. |
1574 | */ |
1575 | static char_u *menu_translate_tab_and_shift(char_u *arg_start) |
1576 | { |
1577 | char_u *arg = arg_start; |
1578 | |
1579 | while (*arg && !ascii_iswhite(*arg)) { |
1580 | if ((*arg == '\\' || *arg == Ctrl_V) && arg[1] != NUL) |
1581 | arg++; |
1582 | else if (STRNICMP(arg, "<TAB>" , 5) == 0) { |
1583 | *arg = TAB; |
1584 | STRMOVE(arg + 1, arg + 5); |
1585 | } |
1586 | arg++; |
1587 | } |
1588 | if (*arg != NUL) |
1589 | *arg++ = NUL; |
1590 | arg = skipwhite(arg); |
1591 | |
1592 | return arg; |
1593 | } |
1594 | |
1595 | |