/* infodoc.c -- functions which build documentation nodes. Copyright 1993-2023 Free Software Foundation, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Originally written by Brian Fox. */ #include "info.h" #include "scan.h" #include "util.h" #include "filesys.h" #include "session.h" #include "doc.h" #include "funs.h" /* The name of the node used in the help window. */ static char *info_help_nodename = "*Info Help*"; /* A node containing printed key bindings and their documentation. */ static NODE *internal_info_help_node = NULL; /* The (more or less) static text which appears in the internal info help node. The actual key bindings are inserted. Keep the underlines (****, etc.) in the same N_ call as the text lines they refer to, so translations can make the number of *'s or -'s match. */ static char *info_internal_help_text[] = { N_("Basic Info command keys\n"), "\n", N_("\\%-10[quit-help] Close this help window.\n"), N_("\\%-10[quit] Quit Info altogether.\n"), N_("\\%-10[get-info-help-node] Invoke the Info tutorial.\n"), "\n", N_("\\%-10[prev-line] Move up one line.\n"), N_("\\%-10[next-line] Move down one line.\n"), N_("\\%-10[scroll-backward] Scroll backward one screenful.\n"), N_("\\%-10[scroll-forward] Scroll forward one screenful.\n"), N_("\\%-10[beginning-of-node] Go to the beginning of this node.\n"), N_("\\%-10[end-of-node] Go to the end of this node.\n"), "\n", N_("\\%-10[move-to-next-xref] Skip to the next hypertext link.\n"), N_("\\%-10[select-reference-this-line] Follow the hypertext link under the cursor.\n"), N_("\\%-10[history-node] Go back to the last node seen in this window.\n"), "\n", N_("\\%-10[global-prev-node] Go to the previous node in the document.\n"), N_("\\%-10[global-next-node] Go to the next node in the document.\n"), N_("\\%-10[prev-node] Go to the previous node on this level.\n"), N_("\\%-10[next-node] Go to the next node on this level.\n"), N_("\\%-10[up-node] Go up one level.\n"), N_("\\%-10[top-node] Go to the top node of this document.\n"), N_("\\%-10[dir-node] Go to the main 'directory' node.\n"), "\n", N_("1...9 Pick the first...ninth item in this node's menu.\n"), N_("\\%-10[last-menu-item] Pick the last item in this node's menu.\n"), N_("\\%-10[menu-item] Pick a menu item specified by name.\n"), N_("\\%-10[xref-item] Follow a cross reference specified by name.\n"), N_("\\%-10[goto-node] Go to a node specified by name.\n"), "\n", N_("\\%-10[search] Search forward for a specified string.\n"), N_("\\%-10[search-previous] Search for previous occurrence.\n"), N_("\\%-10[search-next] Search for next occurrence.\n"), N_("\\%-10[index-search] Search for a specified string in the index, and\n\ select the node referenced by the first entry found.\n"), N_("\\%-10[virtual-index] Synthesize menu of matching index entries.\n"), "\n", N_("\\%-10[abort-key] Cancel the current operation.\n"), "\n", NULL }; static char *where_is_internal (Keymap map, InfoCommand *cmd); static void dump_map_to_text_buffer (struct text_buffer *tb, int *prefix, int prefix_len, Keymap map) { register int i; int *new_prefix = xmalloc ((prefix_len + 2) * sizeof (int)); memcpy (new_prefix, prefix, prefix_len * sizeof (int)); new_prefix[prefix_len + 1] = 0; for (i = 0; i < KEYMAP_SIZE; i++) { if (i == 128) i = 256; if (i == 128 + KEYMAP_META_BASE) i = 256 + KEYMAP_META_BASE; new_prefix[prefix_len] = i; if (map[i].type == ISKMAP) { dump_map_to_text_buffer (tb, new_prefix, prefix_len + 1, map[i].value.keymap); } else if (map[i].value.function) { long start_of_line = tb->off; register int last; char *doc, *name; /* Hide some key mappings. */ if (map[i].value.function && (map[i].value.function->func == info_do_lowercase_version)) continue; doc = function_documentation (map[i].value.function); name = function_name (map[i].value.function); if (!*doc) continue; /* Find out if there is a series of identical functions, as in add-digit-to-numeric-arg. */ for (last = i + 1; last < KEYMAP_SIZE; last++) if ((map[last].type != ISFUNC) || (map[last].value.function != map[i].value.function)) break; if (last - 1 != i) { text_buffer_printf (tb, "%s .. ", pretty_keyseq (new_prefix)); new_prefix[prefix_len] = last - 1; text_buffer_printf (tb, "%s", pretty_keyseq (new_prefix)); i = last - 1; } else text_buffer_printf (tb, "%s", pretty_keyseq (new_prefix)); while (tb->off - start_of_line < 8) text_buffer_printf (tb, " "); /* Print the name of the function, and some padding before the documentation string is printed. */ { int length_so_far; int desired_doc_start = 40; text_buffer_printf (tb, "(%s)", name); length_so_far = tb->off - start_of_line; if ((desired_doc_start + strlen (doc)) >= (unsigned int) the_screen->width) text_buffer_printf (tb, "\n "); else { while (length_so_far < desired_doc_start) { text_buffer_printf (tb, " "); length_so_far++; } } } text_buffer_printf (tb, "%s\n", doc); } } free (new_prefix); } /* How to create internal_info_help_node. HELP_IS_ONLY_WINDOW_P says whether we're going to end up in a second (or more) window of our own, or whether there's only one window and we're going to usurp it. This determines how to quit the help window. Maybe we should just make q do the right thing in both cases. */ static void create_internal_info_help_node (int help_is_only_window_p) { register int i; NODE *node; char *exec_keys; int printed_one_mx = 0; struct text_buffer msg; char *infopath_str = infopath_string (); text_buffer_init (&msg); for (i = 0; info_internal_help_text[i]; i++) text_buffer_printf (&msg, replace_in_documentation (_(info_internal_help_text[i]), help_is_only_window_p), NULL, NULL, NULL); text_buffer_printf (&msg, "---------------------\n"); text_buffer_printf (&msg, _("This is GNU Info version %s. "), VERSION); text_buffer_printf (&msg, _("The current search path is:\n")); text_buffer_printf (&msg, "%s\n", infopath_str); text_buffer_printf (&msg, "---------------------\n\n"); free (infopath_str); text_buffer_printf (&msg, _("Commands available in Info windows:\n\n")); dump_map_to_text_buffer (&msg, 0, 0, info_keymap); text_buffer_printf (&msg, "---------------------\n\n"); text_buffer_printf (&msg, _("Commands available in the echo area:\n\n")); dump_map_to_text_buffer (&msg, 0, 0, echo_area_keymap); /* Get a list of commands which have no keystroke equivs. */ exec_keys = where_is (info_keymap, InfoCmd(info_execute_command)); if (exec_keys) exec_keys = xstrdup (exec_keys); for (i = 0; function_doc_array[i].func; i++) { InfoCommand *cmd = &function_doc_array[i]; if (cmd->func != info_do_lowercase_version && !where_is_internal (info_keymap, cmd) && !where_is_internal (echo_area_keymap, cmd)) { if (!printed_one_mx) { text_buffer_printf (&msg, "---------------------\n\n"); if (exec_keys && exec_keys[0]) text_buffer_printf (&msg, _("The following commands can only be invoked via " "%s:\n\n"), exec_keys); else text_buffer_printf (&msg, _("The following commands cannot be invoked at all:\n\n")); printed_one_mx = 1; } text_buffer_printf (&msg, "%s %s\n %s\n", exec_keys, function_doc_array[i].func_name, replace_in_documentation (strlen (function_doc_array[i].doc) ? _(function_doc_array[i].doc) : "", 0) ); } } free (exec_keys); node = text_buffer_to_node (&msg); internal_info_help_node = node; name_internal_node (internal_info_help_node, xstrdup (info_help_nodename)); } /* Return a window which is the window showing help in this Info. */ /* If the eligible window's height is >= this, split it to make the help window. Otherwise display the help window in the current window. */ #define HELP_SPLIT_SIZE 24 static WINDOW * info_find_or_create_help_window (void) { int help_is_only_window_p; WINDOW *eligible = NULL; WINDOW *help_window = get_internal_info_window (info_help_nodename); /* Close help window if in it already. */ if (help_window && help_window == active_window) { info_delete_window_internal (help_window); return NULL; } /* If we couldn't find the help window, then make it. */ if (!help_window) { WINDOW *window; int max = 0; for (window = windows; window; window = window->next) { if (window->height > max) { max = window->height; eligible = window; } } if (!eligible) { info_error ("%s", msg_cant_make_help); return NULL; } } /* Make sure that we have a node containing the help text. The argument is false if help will be the only window (so l must be used to quit help), true if help will be one of several visible windows (so CTRL-x 0 must be used to quit help). */ help_is_only_window_p = ((help_window && !windows->next) || (!help_window && eligible->height < HELP_SPLIT_SIZE)); create_internal_info_help_node (help_is_only_window_p); /* Either use the existing window to display the help node, or create a new window if there was no existing help window. */ if (!help_window) { /* Split the largest window into 2 windows, and show the help text in that window. */ if (eligible->height >= HELP_SPLIT_SIZE) { active_window = eligible; help_window = window_make_window (); info_set_node_of_window (help_window, internal_info_help_node); } else { info_set_node_of_window (active_window, internal_info_help_node); help_window = active_window; } } else { /* Case where help node always gets regenerated, and we have an existing window in which to place the node. */ if (active_window != help_window) { active_window = help_window; } info_set_node_of_window (active_window, internal_info_help_node); } return help_window; } /* Create or move to the help window. */ DECLARE_INFO_COMMAND (info_get_help_window, _("Display help message")) { WINDOW *help_window; help_window = info_find_or_create_help_window (); if (help_window) { active_window = help_window; } } /* Show the Info help node. This means that the "info" file is installed where it can easily be found on your system. */ DECLARE_INFO_COMMAND (info_get_info_help_node, _("Visit Info node '(info)Help'")) { NODE *node; char *nodename; /* If there is a window on the screen showing the node "(info)Help" or the node "(info)Help-Small-Screen", simply select that window. */ { WINDOW *win; for (win = windows; win; win = win->next) { if (win->node && win->node->fullpath && !mbscasecmp ("info", filename_non_directory (win->node->fullpath)) && (!strcmp (win->node->nodename, "Help") || !strcmp (win->node->nodename, "Help-Small-Screen"))) { active_window = win; return; } } } /* If there is more than one window on the screen, check if the user typed "H" for help message before typing "h" for tutorial. If so, close help message so the tutorial will not be in a small window. */ if (windows->next) { WINDOW *help_window = get_internal_info_window (info_help_nodename); if (help_window && help_window == active_window) { info_delete_window_internal (help_window); } } /* If the current window is small, show the small screen help. */ if (active_window->height < 24) nodename = "Help-Small-Screen"; else nodename = "Help"; /* Try to get the info file for Info. */ node = info_get_node ("info", nodename); /* info.info is distributed with Emacs, not Texinfo, so fall back to info-stnd.info if it isn't there. */ if (!node) node = info_get_node ("info-stnd", "Top"); if (!node) { if (info_recent_file_error) info_error ("%s", info_recent_file_error); else info_error (msg_cant_file_node, "info", nodename); return; } info_set_node_of_window (active_window, node); } /* **************************************************************** */ /* */ /* Groveling Info Keymaps and Docs */ /* */ /* **************************************************************** */ /* Return the documentation associated with the Info command FUNCTION. */ char * function_documentation (InfoCommand *cmd) { char *doc; doc = cmd->doc; return replace_in_documentation ((strlen (doc) == 0) ? doc : _(doc), 0); } /* Return the user-visible name of the function associated with the Info command FUNCTION. */ char * function_name (InfoCommand *cmd) { return cmd->func_name; } /* Return a pointer to the info command for function NAME. */ InfoCommand * named_function (char *name) { register int i; for (i = 0; function_doc_array[i].func; i++) if (strcmp (function_doc_array[i].func_name, name) == 0) break; if (!function_doc_array[i].func) return 0; else return &function_doc_array[i]; } DECLARE_INFO_COMMAND (describe_key, _("Print documentation for KEY")) { int keys[50]; int keystroke; int *k = keys; Keymap map = info_keymap; *k = '\0'; for (;;) { message_in_echo_area (_("Describe key: %s"), pretty_keyseq (keys)); keystroke = get_input_key (); unmessage_in_echo_area (); /* Add the KEYSTROKE to our list. */ *k++ = keystroke; *k = '\0'; if (map[keystroke].value.function == NULL) { message_in_echo_area (_("%s is undefined"), pretty_keyseq (keys)); return; } else if (map[keystroke].type == ISKMAP) { map = map[keystroke].value.keymap; continue; } else { char *keyname, *message, *fundoc, *funname = ""; /* If the key is bound to do-lowercase-version, but its lower-case variant is undefined, say that this key is also undefined. This is especially important for unbound edit keys that emit an escape sequence: it's terribly confusing to see a message "Home (do-lowercase-version)" or some such when Home is unbound. */ if (map[keystroke].value.function && map[keystroke].value.function->func == info_do_lowercase_version) { int lowerkey; if (keystroke >= KEYMAP_META_BASE) { lowerkey = keystroke; lowerkey -= KEYMAP_META_BASE; lowerkey = tolower (lowerkey); lowerkey += KEYMAP_META_BASE; } else lowerkey = tolower (keystroke); if (map[lowerkey].value.function == NULL) { message_in_echo_area (_("%s is undefined"), pretty_keyseq (keys)); return; } } keyname = pretty_keyseq (keys); funname = function_name (map[keystroke].value.function); fundoc = function_documentation (map[keystroke].value.function); message = xmalloc (10 + strlen (keyname) + strlen (fundoc) + strlen (funname)); sprintf (message, "%s (%s): %s.", keyname, funname, fundoc); window_message_in_echo_area ("%s", message); free (message); break; } } } /* Return the pretty-printable name of a single key. */ char * pretty_keyname (int key) { static char rep_buffer[30]; char *rep; if (key >= KEYMAP_META_BASE) { char temp[20]; rep = pretty_keyname (key - KEYMAP_META_BASE); sprintf (temp, "M-%s", rep); strcpy (rep_buffer, temp); rep = rep_buffer; } else if (Control_p (key)) { switch (key) { case '\n': rep = "LFD"; break; case '\t': rep = "TAB"; break; case '\r': rep = "RET"; break; case ESC: rep = "ESC"; break; default: sprintf (rep_buffer, "C-%c", UnControl (key)); rep = rep_buffer; } } else if (key >= 256) switch (key) { case KEY_RIGHT_ARROW: rep = "Right"; break; case KEY_LEFT_ARROW: rep = "Left"; break; case KEY_UP_ARROW: rep = "Up"; break; case KEY_DOWN_ARROW: rep = "Down"; break; case KEY_PAGE_UP: rep = "PgUp"; break; case KEY_PAGE_DOWN: rep = "PgDn"; break; case KEY_HOME: rep = "Home"; break; case KEY_END: rep = "End"; break; case KEY_DELETE: rep = "DEL"; break; case KEY_INSERT: rep = "INS"; break; case KEY_BACK_TAB: rep = "BackTab"; break; case KEY_MOUSE: rep = "(mouse event)"; break; default: rep = "(unknown key)"; break; /* This shouldn't be displayed. */ } else { switch (key) { case ' ': rep = "SPC"; break; case DEL: rep = "DEL"; break; default: rep_buffer[0] = key; rep_buffer[1] = '\0'; rep = rep_buffer; } } return rep; } /* Return the pretty printable string which represents KEYSEQ. Return value should not be freed by caller. */ char * pretty_keyseq (int *keyseq) { static struct text_buffer rep = { 0 }; if (!text_buffer_base (&rep)) text_buffer_init (&rep); else text_buffer_reset (&rep); if (!*keyseq) return ""; while (1) { text_buffer_printf (&rep, "%s", pretty_keyname (keyseq[0])); keyseq++; if (!*keyseq) break; text_buffer_add_char (&rep, ' '); } return text_buffer_base (&rep); } /* Replace the names of functions with the key that invokes them. Return value should not be freed by caller. */ char * replace_in_documentation (const char *string, int help_is_only_window_p) { register int i, start; static struct text_buffer txtresult = {0}; text_buffer_free (&txtresult); text_buffer_init (&txtresult); text_buffer_alloc (&txtresult, strlen (string)); start = 0; /* Skip to the beginning of a replaceable function. */ for (i = start; string[i]; i++) { int j = i + 1; /* Is this the start of a replaceable function name? */ if (string[i] == '\\') { char *fmt = NULL; if(string[j] == '%') { if (string[++j] == '-') j++; if (isdigit(string[j])) { while (isdigit(string[j])) j++; if (string[j] == '.' && isdigit(string[j + 1])) { j += 1; while (isdigit(string[j])) j++; } fmt = xmalloc (j - i + 2); strncpy (fmt, string + i + 1, j - i); fmt[j - i - 1] = 's'; fmt[j - i] = '\0'; } else j = i + 1; } if (string[j] == '[') { char *rep_name, *fun_name, *rep; InfoCommand *command; unsigned replen; /* Copy in the old text. */ text_buffer_add_string (&txtresult, string + start, i - start); start = j + 1; /* Move to the end of the function name. */ for (i = start; string[i] && (string[i] != ']'); i++); rep_name = xmalloc (1 + i - start); strncpy (rep_name, string + start, i - start); rep_name[i - start] = '\0'; start = i; if (string[start] == ']') start++; fun_name = rep_name; if (strcmp (rep_name, "quit-help") == 0) { /* Special case for help window. If we have only one window (because the window size was too small to split it), we have to quit help by going back one node in the history list, not deleting the window. */ fun_name = help_is_only_window_p ? "history-node" : "get-help-window"; } /* Find a key which invokes this function in the info_keymap. */ command = named_function (fun_name); /* If the internal documentation string fails, there is a problem with the associated command's documentation (probably in the translation). */ if (!command) { info_error ("bug: no command <%s>\n", rep_name); sleep (1); rep = "BUG"; } else { rep = where_is (info_keymap, command); if (!rep) rep = "N/A"; } replen = strlen (rep); free (rep_name); if (fmt) text_buffer_printf (&txtresult, fmt, rep); else text_buffer_add_string (&txtresult, rep, replen); } free (fmt); } } text_buffer_add_string (&txtresult, string + start, strlen (string + start) + 1); return text_buffer_base (&txtresult); } /* Return a string of characters which could be typed from the keymap MAP to invoke FUNCTION. */ static char *where_is_rep = NULL; static int where_is_rep_index = 0; static int where_is_rep_size = 0; char * where_is (Keymap map, InfoCommand *cmd) { char *rep; if (!where_is_rep_size) where_is_rep = xmalloc (where_is_rep_size = 100); where_is_rep_index = 0; rep = where_is_internal (map, cmd); /* If it couldn't be found, return "M-x Foo" (or equivalent). */ if (!rep) { char *name; name = function_name (cmd); if (!name) return NULL; /* no such function */ rep = where_is_internal (map, InfoCmd(info_execute_command)); if (!rep) return ""; /* function exists but can't be got to by user */ sprintf (where_is_rep, "%s %s", rep, name); rep = where_is_rep; } return rep; } /* Return the printed rep of the keystrokes that invoke FUNCTION, as found in MAP, or NULL. */ static char * where_is_internal (Keymap map, InfoCommand *cmd) { register FUNCTION_KEYSEQ *k; for (k = cmd->keys; k; k = k->next) if (k->map == map) return pretty_keyseq (k->keyseq); return NULL; } DECLARE_INFO_COMMAND (info_where_is, _("Show what to type to execute a given command")) { char *command_name; command_name = read_function_name (_("Where is command: "), window); if (!command_name) { info_abort_key (active_window, count); return; } if (*command_name) { InfoCommand *command; command = named_function (command_name); if (command) { char *location; location = where_is (info_keymap, command); if (!location || !location[0]) { info_error (_("'%s' is not on any keys"), command_name); } else { if (strstr (location, function_name (command))) window_message_in_echo_area (_("%s can only be invoked via %s"), command_name, location); else window_message_in_echo_area (_("%s can be invoked via %s"), command_name, location); } } else info_error (_("There is no function named '%s'"), command_name); } free (command_name); }