diff options
Diffstat (limited to 'src/nvim/help.c')
-rw-r--r-- | src/nvim/help.c | 1178 |
1 files changed, 1178 insertions, 0 deletions
diff --git a/src/nvim/help.c b/src/nvim/help.c new file mode 100644 index 0000000000..172d389e74 --- /dev/null +++ b/src/nvim/help.c @@ -0,0 +1,1178 @@ +// This is an open source non-commercial project. Dear PVS-Studio, please check +// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + +// help.c: functions for Vim help + +#include <stdio.h> +#include <stdlib.h> + +#include "nvim/buffer.h" +#include "nvim/charset.h" +#include "nvim/ex_cmds.h" +#include "nvim/ex_docmd.h" +#include "nvim/ex_getln.h" +#include "nvim/fileio.h" +#include "nvim/garray.h" +#include "nvim/globals.h" +#include "nvim/help.h" +#include "nvim/memory.h" +#include "nvim/option.h" +#include "nvim/os/input.h" +#include "nvim/path.h" +#include "nvim/strings.h" +#include "nvim/syntax.h" +#include "nvim/tag.h" +#include "nvim/vim.h" +#include "nvim/window.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "help.c.generated.h" +#endif + +/// ":help": open a read-only window on a help file +void ex_help(exarg_T *eap) +{ + char *arg; + char *tag; + FILE *helpfd; // file descriptor of help file + int n; + int i; + win_T *wp; + int num_matches; + char **matches; + char *p; + int empty_fnum = 0; + int alt_fnum = 0; + buf_T *buf; + int len; + char *lang; + const bool old_KeyTyped = KeyTyped; + + if (eap != NULL) { + // A ":help" command ends at the first LF, or at a '|' that is + // followed by some text. Set nextcmd to the following command. + for (arg = eap->arg; *arg; arg++) { + if (*arg == '\n' || *arg == '\r' + || (*arg == '|' && arg[1] != NUL && arg[1] != '|')) { + *arg++ = NUL; + eap->nextcmd = arg; + break; + } + } + arg = eap->arg; + + if (eap->forceit && *arg == NUL && !curbuf->b_help) { + emsg(_("E478: Don't panic!")); + return; + } + + if (eap->skip) { // not executing commands + return; + } + } else { + arg = ""; + } + + // remove trailing blanks + p = arg + STRLEN(arg) - 1; + while (p > arg && ascii_iswhite(*p) && p[-1] != '\\') { + *p-- = NUL; + } + + // Check for a specified language + lang = check_help_lang(arg); + + // When no argument given go to the index. + if (*arg == NUL) { + arg = "help.txt"; + } + + // Check if there is a match for the argument. + n = find_help_tags(arg, &num_matches, &matches, eap != NULL && eap->forceit); + + i = 0; + if (n != FAIL && lang != NULL) { + // Find first item with the requested language. + for (i = 0; i < num_matches; i++) { + len = (int)STRLEN(matches[i]); + if (len > 3 && matches[i][len - 3] == '@' + && STRICMP(matches[i] + len - 2, lang) == 0) { + break; + } + } + } + if (i >= num_matches || n == FAIL) { + if (lang != NULL) { + semsg(_("E661: Sorry, no '%s' help for %s"), lang, arg); + } else { + semsg(_("E149: Sorry, no help for %s"), arg); + } + if (n != FAIL) { + FreeWild(num_matches, matches); + } + return; + } + + // The first match (in the requested language) is the best match. + tag = xstrdup(matches[i]); + FreeWild(num_matches, matches); + + // Re-use an existing help window or open a new one. + // Always open a new one for ":tab help". + if (!bt_help(curwin->w_buffer) || cmdmod.cmod_tab != 0) { + if (cmdmod.cmod_tab != 0) { + wp = NULL; + } else { + wp = NULL; + FOR_ALL_WINDOWS_IN_TAB(wp2, curtab) { + if (bt_help(wp2->w_buffer)) { + wp = wp2; + break; + } + } + } + if (wp != NULL && wp->w_buffer->b_nwindows > 0) { + win_enter(wp, true); + } else { + // There is no help window yet. + // Try to open the file specified by the "helpfile" option. + if ((helpfd = os_fopen((char *)p_hf, READBIN)) == NULL) { + smsg(_("Sorry, help file \"%s\" not found"), p_hf); + goto erret; + } + fclose(helpfd); + + // Split off help window; put it at far top if no position + // specified, the current window is vertically split and + // narrow. + n = WSP_HELP; + if (cmdmod.cmod_split == 0 && curwin->w_width != Columns + && curwin->w_width < 80) { + n |= WSP_TOP; + } + if (win_split(0, n) == FAIL) { + goto erret; + } + + if (curwin->w_height < p_hh) { + win_setheight((int)p_hh); + } + + // Open help file (do_ecmd() will set b_help flag, readfile() will + // set b_p_ro flag). + // Set the alternate file to the previously edited file. + alt_fnum = curbuf->b_fnum; + (void)do_ecmd(0, NULL, NULL, NULL, ECMD_LASTL, + ECMD_HIDE + ECMD_SET_HELP, + NULL); // buffer is still open, don't store info + + if ((cmdmod.cmod_flags & CMOD_KEEPALT) == 0) { + curwin->w_alt_fnum = alt_fnum; + } + empty_fnum = curbuf->b_fnum; + } + } + + restart_edit = 0; // don't want insert mode in help file + + // Restore KeyTyped, setting 'filetype=help' may reset it. + // It is needed for do_tag top open folds under the cursor. + KeyTyped = old_KeyTyped; + + do_tag((char_u *)tag, DT_HELP, 1, false, true); + + // Delete the empty buffer if we're not using it. Careful: autocommands + // may have jumped to another window, check that the buffer is not in a + // window. + if (empty_fnum != 0 && curbuf->b_fnum != empty_fnum) { + buf = buflist_findnr(empty_fnum); + if (buf != NULL && buf->b_nwindows == 0) { + wipe_buffer(buf, true); + } + } + + // keep the previous alternate file + if (alt_fnum != 0 && curwin->w_alt_fnum == empty_fnum + && (cmdmod.cmod_flags & CMOD_KEEPALT) == 0) { + curwin->w_alt_fnum = alt_fnum; + } + +erret: + xfree(tag); +} + +/// ":helpclose": Close one help window +void ex_helpclose(exarg_T *eap) +{ + FOR_ALL_WINDOWS_IN_TAB(win, curtab) { + if (bt_help(win->w_buffer)) { + win_close(win, false, eap->forceit); + return; + } + } +} + +/// In an argument search for a language specifiers in the form "@xx". +/// Changes the "@" to NUL if found, and returns a pointer to "xx". +/// +/// @return NULL if not found. +char *check_help_lang(char *arg) +{ + int len = (int)STRLEN(arg); + + if (len >= 3 && arg[len - 3] == '@' && ASCII_ISALPHA(arg[len - 2]) + && ASCII_ISALPHA(arg[len - 1])) { + arg[len - 3] = NUL; // remove the '@' + return arg + len - 2; + } + return NULL; +} + +/// Return a heuristic indicating how well the given string matches. The +/// smaller the number, the better the match. This is the order of priorities, +/// from best match to worst match: +/// - Match with least alphanumeric characters is better. +/// - Match with least total characters is better. +/// - Match towards the start is better. +/// - Match starting with "+" is worse (feature instead of command) +/// Assumption is made that the matched_string passed has already been found to +/// match some string for which help is requested. webb. +/// +/// @param offset offset for match +/// @param wrong_case no matching case +/// +/// @return a heuristic indicating how well the given string matches. +int help_heuristic(char *matched_string, int offset, int wrong_case) + FUNC_ATTR_PURE +{ + int num_letters; + char *p; + + num_letters = 0; + for (p = matched_string; *p; p++) { + if (ASCII_ISALNUM(*p)) { + num_letters++; + } + } + + // Multiply the number of letters by 100 to give it a much bigger + // weighting than the number of characters. + // If there only is a match while ignoring case, add 5000. + // If the match starts in the middle of a word, add 10000 to put it + // somewhere in the last half. + // If the match is more than 2 chars from the start, multiply by 200 to + // put it after matches at the start. + if (offset > 0 + && ASCII_ISALNUM(matched_string[offset]) + && ASCII_ISALNUM(matched_string[offset - 1])) { + offset += 10000; + } else if (offset > 2) { + offset *= 200; + } + if (wrong_case) { + offset += 5000; + } + // Features are less interesting than the subjects themselves, but "+" + // alone is not a feature. + if (matched_string[0] == '+' && matched_string[1] != NUL) { + offset += 100; + } + return 100 * num_letters + (int)STRLEN(matched_string) + offset; +} + +/// Compare functions for qsort() below, that checks the help heuristics number +/// that has been put after the tagname by find_tags(). +static int help_compare(const void *s1, const void *s2) +{ + char *p1; + char *p2; + + p1 = *(char **)s1 + strlen(*(char **)s1) + 1; + p2 = *(char **)s2 + strlen(*(char **)s2) + 1; + + // Compare by help heuristic number first. + int cmp = strcmp(p1, p2); + if (cmp != 0) { + return cmp; + } + + // Compare by strings as tie-breaker when same heuristic number. + return strcmp(*(char **)s1, *(char **)s2); +} + +/// Find all help tags matching "arg", sort them and return in matches[], with +/// the number of matches in num_matches. +/// The matches will be sorted with a "best" match algorithm. +/// When "keep_lang" is true try keeping the language of the current buffer. +int find_help_tags(const char *arg, int *num_matches, char ***matches, bool keep_lang) +{ + int i; + + // Specific tags that either have a specific replacement or won't go + // through the generic rules. + static char *(except_tbl[][2]) = { + { "*", "star" }, + { "g*", "gstar" }, + { "[*", "[star" }, + { "]*", "]star" }, + { ":*", ":star" }, + { "/*", "/star" }, // NOLINT + { "/\\*", "/\\\\star" }, + { "\"*", "quotestar" }, + { "**", "starstar" }, + { "cpo-*", "cpo-star" }, + { "/\\(\\)", "/\\\\(\\\\)" }, + { "/\\%(\\)", "/\\\\%(\\\\)" }, + { "?", "?" }, + { "??", "??" }, + { ":?", ":?" }, + { "?<CR>", "?<CR>" }, + { "g?", "g?" }, + { "g?g?", "g?g?" }, + { "g??", "g??" }, + { "-?", "-?" }, + { "q?", "q?" }, + { "v_g?", "v_g?" }, + { "/\\?", "/\\\\?" }, + { "/\\z(\\)", "/\\\\z(\\\\)" }, + { "\\=", "\\\\=" }, + { ":s\\=", ":s\\\\=" }, + { "[count]", "\\[count]" }, + { "[quotex]", "\\[quotex]" }, + { "[range]", "\\[range]" }, + { ":[range]", ":\\[range]" }, + { "[pattern]", "\\[pattern]" }, + { "\\|", "\\\\bar" }, + { "\\%$", "/\\\\%\\$" }, + { "s/\\~", "s/\\\\\\~" }, + { "s/\\U", "s/\\\\U" }, + { "s/\\L", "s/\\\\L" }, + { "s/\\1", "s/\\\\1" }, + { "s/\\2", "s/\\\\2" }, + { "s/\\3", "s/\\\\3" }, + { "s/\\9", "s/\\\\9" }, + { NULL, NULL } + }; + + static const char *(expr_table[]) = { + "!=?", "!~?", "<=?", "<?", "==?", "=~?", + ">=?", ">?", "is?", "isnot?" + }; + char *d = (char *)IObuff; // assume IObuff is long enough! + d[0] = NUL; + + if (STRNICMP(arg, "expr-", 5) == 0) { + // When the string starting with "expr-" and containing '?' and matches + // the table, it is taken literally (but ~ is escaped). Otherwise '?' + // is recognized as a wildcard. + for (i = (int)ARRAY_SIZE(expr_table); --i >= 0;) { + if (STRCMP(arg + 5, expr_table[i]) == 0) { + for (int si = 0, di = 0;; si++) { + if (arg[si] == '~') { + d[di++] = '\\'; + } + d[di++] = arg[si]; + if (arg[si] == NUL) { + break; + } + } + break; + } + } + } else { + // Recognize a few exceptions to the rule. Some strings that contain + // '*'are changed to "star", otherwise '*' is recognized as a wildcard. + for (i = 0; except_tbl[i][0] != NULL; i++) { + if (STRCMP(arg, except_tbl[i][0]) == 0) { + STRCPY(d, except_tbl[i][1]); + break; + } + } + } + + if (d[0] == NUL) { // no match in table + // Replace "\S" with "/\\S", etc. Otherwise every tag is matched. + // Also replace "\%^" and "\%(", they match every tag too. + // Also "\zs", "\z1", etc. + // Also "\@<", "\@=", "\@<=", etc. + // And also "\_$" and "\_^". + if (arg[0] == '\\' + && ((arg[1] != NUL && arg[2] == NUL) + || (vim_strchr("%_z@", arg[1]) != NULL + && arg[2] != NUL))) { + vim_snprintf(d, IOSIZE, "/\\\\%s", arg + 1); + // Check for "/\\_$", should be "/\\_\$" + if (d[3] == '_' && d[4] == '$') { + STRCPY(d + 4, "\\$"); + } + } else { + // Replace: + // "[:...:]" with "\[:...:]" + // "[++...]" with "\[++...]" + // "\{" with "\\{" -- matching "} \}" + if ((arg[0] == '[' && (arg[1] == ':' + || (arg[1] == '+' && arg[2] == '+'))) + || (arg[0] == '\\' && arg[1] == '{')) { + *d++ = '\\'; + } + + // If tag starts with "('", skip the "(". Fixes CTRL-] on ('option'. + if (*arg == '(' && arg[1] == '\'') { + arg++; + } + for (const char *s = arg; *s; s++) { + // Replace "|" with "bar" and '"' with "quote" to match the name of + // the tags for these commands. + // Replace "*" with ".*" and "?" with "." to match command line + // completion. + // Insert a backslash before '~', '$' and '.' to avoid their + // special meaning. + if ((char_u *)d - IObuff > IOSIZE - 10) { // getting too long!? + break; + } + switch (*s) { + case '|': + STRCPY(d, "bar"); + d += 3; + continue; + case '"': + STRCPY(d, "quote"); + d += 5; + continue; + case '*': + *d++ = '.'; + break; + case '?': + *d++ = '.'; + continue; + case '$': + case '.': + case '~': + *d++ = '\\'; + break; + } + + // Replace "^x" by "CTRL-X". Don't do this for "^_" to make + // ":help i_^_CTRL-D" work. + // Insert '-' before and after "CTRL-X" when applicable. + if (*s < ' ' + || (*s == '^' && s[1] + && (ASCII_ISALPHA(s[1]) || vim_strchr("?@[\\]^", s[1]) != NULL))) { + if ((char_u *)d > IObuff && d[-1] != '_' && d[-1] != '\\') { + *d++ = '_'; // prepend a '_' to make x_CTRL-x + } + STRCPY(d, "CTRL-"); + d += 5; + if (*s < ' ') { + *d++ = (char)(*s + '@'); + if (d[-1] == '\\') { + *d++ = '\\'; // double a backslash + } + } else { + *d++ = *++s; + } + if (s[1] != NUL && s[1] != '_') { + *d++ = '_'; // append a '_' + } + continue; + } else if (*s == '^') { // "^" or "CTRL-^" or "^_" + *d++ = '\\'; + } else if (s[0] == '\\' && s[1] != '\\' && *arg == '/' && s == arg + 1) { + // Insert a backslash before a backslash after a slash, for search + // pattern tags: "/\|" --> "/\\|". + *d++ = '\\'; + } + + // "CTRL-\_" -> "CTRL-\\_" to avoid the special meaning of "\_" in + // "CTRL-\_CTRL-N" + if (STRNICMP(s, "CTRL-\\_", 7) == 0) { + STRCPY(d, "CTRL-\\\\"); + d += 7; + s += 6; + } + + *d++ = *s; + + // If tag contains "({" or "([", tag terminates at the "(". + // This is for help on functions, e.g.: abs({expr}). + if (*s == '(' && (s[1] == '{' || s[1] == '[')) { + break; + } + + // If tag starts with ', toss everything after a second '. Fixes + // CTRL-] on 'option'. (would include the trailing '.'). + if (*s == '\'' && s > arg && *arg == '\'') { + break; + } + // Also '{' and '}'. Fixes CTRL-] on '{address}'. + if (*s == '}' && s > arg && *arg == '{') { + break; + } + } + *d = NUL; + + if (*IObuff == '`') { + if ((char_u *)d > IObuff + 2 && d[-1] == '`') { + // remove the backticks from `command` + memmove(IObuff, IObuff + 1, STRLEN(IObuff)); + d[-2] = NUL; + } else if ((char_u *)d > IObuff + 3 && d[-2] == '`' && d[-1] == ',') { + // remove the backticks and comma from `command`, + memmove(IObuff, IObuff + 1, STRLEN(IObuff)); + d[-3] = NUL; + } else if ((char_u *)d > IObuff + 4 && d[-3] == '`' + && d[-2] == '\\' && d[-1] == '.') { + // remove the backticks and dot from `command`\. + memmove(IObuff, IObuff + 1, STRLEN(IObuff)); + d[-4] = NUL; + } + } + } + } + + *matches = NULL; + *num_matches = 0; + int flags = TAG_HELP | TAG_REGEXP | TAG_NAMES | TAG_VERBOSE | TAG_NO_TAGFUNC; + if (keep_lang) { + flags |= TAG_KEEP_LANG; + } + if (find_tags(IObuff, num_matches, matches, flags, MAXCOL, NULL) == OK + && *num_matches > 0) { + // Sort the matches found on the heuristic number that is after the + // tag name. + qsort((void *)(*matches), (size_t)(*num_matches), + sizeof(char_u *), help_compare); + // Delete more than TAG_MANY to reduce the size of the listing. + while (*num_matches > TAG_MANY) { + xfree((*matches)[--*num_matches]); + } + } + return OK; +} + +/// Cleanup matches for help tags: +/// Remove "@ab" if the top of 'helplang' is "ab" and the language of the first +/// tag matches it. Otherwise remove "@en" if "en" is the only language. +void cleanup_help_tags(int num_file, char **file) +{ + char_u buf[4]; + char_u *p = buf; + + if (p_hlg[0] != NUL && (p_hlg[0] != 'e' || p_hlg[1] != 'n')) { + *p++ = '@'; + *p++ = p_hlg[0]; + *p++ = p_hlg[1]; + } + *p = NUL; + + for (int i = 0; i < num_file; i++) { + int len = (int)STRLEN(file[i]) - 3; + if (len <= 0) { + continue; + } + if (STRCMP(file[i] + len, "@en") == 0) { + // Sorting on priority means the same item in another language may + // be anywhere. Search all items for a match up to the "@en". + int j; + for (j = 0; j < num_file; j++) { + if (j != i + && (int)STRLEN(file[j]) == len + 3 + && STRNCMP(file[i], file[j], len + 1) == 0) { + break; + } + } + if (j == num_file) { + // item only exists with @en, remove it + file[i][len] = NUL; + } + } + } + + if (*buf != NUL) { + for (int i = 0; i < num_file; i++) { + int len = (int)STRLEN(file[i]) - 3; + if (len <= 0) { + continue; + } + if (STRCMP(file[i] + len, buf) == 0) { + // remove the default language + file[i][len] = NUL; + } + } + } +} + +/// Called when starting to edit a buffer for a help file. +void prepare_help_buffer(void) +{ + curbuf->b_help = true; + set_string_option_direct("buftype", -1, "help", OPT_FREE|OPT_LOCAL, 0); + + // Always set these options after jumping to a help tag, because the + // user may have an autocommand that gets in the way. + // Accept all ASCII chars for keywords, except ' ', '*', '"', '|', and + // latin1 word characters (for translated help files). + // Only set it when needed, buf_init_chartab() is some work. + char *p = "!-~,^*,^|,^\",192-255"; + if (STRCMP(curbuf->b_p_isk, p) != 0) { + set_string_option_direct("isk", -1, p, OPT_FREE|OPT_LOCAL, 0); + check_buf_options(curbuf); + (void)buf_init_chartab(curbuf, false); + } + + // Don't use the global foldmethod. + set_string_option_direct("fdm", -1, "manual", OPT_FREE|OPT_LOCAL, 0); + + curbuf->b_p_ts = 8; // 'tabstop' is 8. + curwin->w_p_list = false; // No list mode. + + curbuf->b_p_ma = false; // Not modifiable. + curbuf->b_p_bin = false; // Reset 'bin' before reading file. + curwin->w_p_nu = 0; // No line numbers. + curwin->w_p_rnu = 0; // No relative line numbers. + RESET_BINDING(curwin); // No scroll or cursor binding. + curwin->w_p_arab = false; // No arabic mode. + curwin->w_p_rl = false; // Help window is left-to-right. + curwin->w_p_fen = false; // No folding in the help window. + curwin->w_p_diff = false; // No 'diff'. + curwin->w_p_spell = false; // No spell checking. + + set_buflisted(false); +} + +/// After reading a help file: May cleanup a help buffer when syntax +/// highlighting is not used. +void fix_help_buffer(void) +{ + linenr_T lnum; + char *line; + bool in_example = false; + + // Set filetype to "help". + if (STRCMP(curbuf->b_p_ft, "help") != 0) { + curbuf->b_ro_locked++; + set_option_value("ft", 0L, "help", OPT_LOCAL); + curbuf->b_ro_locked--; + } + + if (!syntax_present(curwin)) { + for (lnum = 1; lnum <= curbuf->b_ml.ml_line_count; lnum++) { + line = (char *)ml_get_buf(curbuf, lnum, false); + const size_t len = STRLEN(line); + if (in_example && len > 0 && !ascii_iswhite(line[0])) { + // End of example: non-white or '<' in first column. + if (line[0] == '<') { + // blank-out a '<' in the first column + line = (char *)ml_get_buf(curbuf, lnum, true); + line[0] = ' '; + } + in_example = false; + } + if (!in_example && len > 0) { + if (line[len - 1] == '>' && (len == 1 || line[len - 2] == ' ')) { + // blank-out a '>' in the last column (start of example) + line = (char *)ml_get_buf(curbuf, lnum, true); + line[len - 1] = ' '; + in_example = true; + } else if (line[len - 1] == '~') { + // blank-out a '~' at the end of line (header marker) + line = (char *)ml_get_buf(curbuf, lnum, true); + line[len - 1] = ' '; + } + } + } + } + + // In the "help.txt" and "help.abx" file, add the locally added help + // files. This uses the very first line in the help file. + char *const fname = path_tail(curbuf->b_fname); + if (FNAMECMP(fname, "help.txt") == 0 + || (FNAMENCMP(fname, "help.", 5) == 0 + && ASCII_ISALPHA(fname[5]) + && ASCII_ISALPHA(fname[6]) + && TOLOWER_ASC(fname[7]) == 'x' + && fname[8] == NUL)) { + for (lnum = 1; lnum < curbuf->b_ml.ml_line_count; lnum++) { + line = (char *)ml_get_buf(curbuf, lnum, false); + if (strstr(line, "*local-additions*") == NULL) { + continue; + } + + // Go through all directories in 'runtimepath', skipping + // $VIMRUNTIME. + char *p = (char *)p_rtp; + while (*p != NUL) { + copy_option_part(&p, (char *)NameBuff, MAXPATHL, ","); + char *const rt = vim_getenv("VIMRUNTIME"); + if (rt != NULL + && path_full_compare(rt, (char *)NameBuff, false, true) != kEqualFiles) { + int fcount; + char **fnames; + char *s; + vimconv_T vc; + char *cp; + + // Find all "doc/ *.txt" files in this directory. + if (!add_pathsep((char *)NameBuff) + || STRLCAT(NameBuff, "doc/*.??[tx]", // NOLINT + sizeof(NameBuff)) >= MAXPATHL) { + emsg(_(e_fnametoolong)); + continue; + } + + // Note: We cannot just do `&NameBuff` because it is a statically sized array + // so `NameBuff == &NameBuff` according to C semantics. + char *buff_list[1] = { (char *)NameBuff }; + if (gen_expand_wildcards(1, buff_list, &fcount, + &fnames, EW_FILE|EW_SILENT) == OK + && fcount > 0) { + // If foo.abx is found use it instead of foo.txt in + // the same directory. + for (int i1 = 0; i1 < fcount; i1++) { + for (int i2 = 0; i2 < fcount; i2++) { + if (i1 == i2) { + continue; + } + if (fnames[i1] == NULL || fnames[i2] == NULL) { + continue; + } + const char *const f1 = fnames[i1]; + const char *const f2 = fnames[i2]; + const char *const t1 = path_tail(f1); + const char *const t2 = path_tail(f2); + const char *const e1 = (char *)STRRCHR(t1, '.'); + const char *const e2 = (char *)STRRCHR(t2, '.'); + if (e1 == NULL || e2 == NULL) { + continue; + } + if (FNAMECMP(e1, ".txt") != 0 + && FNAMECMP(e1, fname + 4) != 0) { + // Not .txt and not .abx, remove it. + XFREE_CLEAR(fnames[i1]); + continue; + } + if (e1 - f1 != e2 - f2 + || FNAMENCMP(f1, f2, e1 - f1) != 0) { + continue; + } + if (FNAMECMP(e1, ".txt") == 0 + && FNAMECMP(e2, fname + 4) == 0) { + // use .abx instead of .txt + XFREE_CLEAR(fnames[i1]); + } + } + } + for (int fi = 0; fi < fcount; fi++) { + if (fnames[fi] == NULL) { + continue; + } + + FILE *const fd = os_fopen(fnames[fi], "r"); + if (fd == NULL) { + continue; + } + vim_fgets(IObuff, IOSIZE, fd); + if (IObuff[0] == '*' + && (s = vim_strchr((char *)IObuff + 1, '*')) + != NULL) { + TriState this_utf = kNone; + // Change tag definition to a + // reference and remove <CR>/<NL>. + IObuff[0] = '|'; + *s = '|'; + while (*s != NUL) { + if (*s == '\r' || *s == '\n') { + *s = NUL; + } + // The text is utf-8 when a byte + // above 127 is found and no + // illegal byte sequence is found. + if ((char_u)(*s) >= 0x80 && this_utf != kFalse) { + this_utf = kTrue; + const int l = utf_ptr2len(s); + if (l == 1) { + this_utf = kFalse; + } + s += l - 1; + } + s++; + } + // The help file is latin1 or utf-8; + // conversion to the current + // 'encoding' may be required. + vc.vc_type = CONV_NONE; + convert_setup(&vc, + (char_u *)(this_utf == kTrue ? "utf-8" : "latin1"), + p_enc); + if (vc.vc_type == CONV_NONE) { + // No conversion needed. + cp = (char *)IObuff; + } else { + // Do the conversion. If it fails + // use the unconverted text. + cp = (char *)string_convert(&vc, IObuff, NULL); + if (cp == NULL) { + cp = (char *)IObuff; + } + } + convert_setup(&vc, NULL, NULL); + + ml_append(lnum, cp, (colnr_T)0, false); + if ((char_u *)cp != IObuff) { + xfree(cp); + } + lnum++; + } + fclose(fd); + } + FreeWild(fcount, fnames); + } + } + xfree(rt); + } + break; + } + } +} + +/// ":exusage" +void ex_exusage(exarg_T *eap) +{ + do_cmdline_cmd("help ex-cmd-index"); +} + +/// ":viusage" +void ex_viusage(exarg_T *eap) +{ + do_cmdline_cmd("help normal-index"); +} + +/// Generate tags in one help directory +/// +/// @param dir Path to the doc directory +/// @param ext Suffix of the help files (".txt", ".itx", ".frx", etc.) +/// @param tagname Name of the tags file ("tags" for English, "tags-fr" for +/// French) +/// @param add_help_tags Whether to add the "help-tags" tag +/// @param ignore_writeerr ignore write error +static void helptags_one(char *dir, const char *ext, const char *tagfname, bool add_help_tags, + bool ignore_writeerr) + FUNC_ATTR_NONNULL_ALL +{ + garray_T ga; + int filecount; + char **files; + char *p1, *p2; + char *s; + TriState utf8 = kNone; + bool mix = false; // detected mixed encodings + + // Find all *.txt files. + size_t dirlen = STRLCPY(NameBuff, dir, sizeof(NameBuff)); + if (dirlen >= MAXPATHL + || STRLCAT(NameBuff, "/**/*", sizeof(NameBuff)) >= MAXPATHL // NOLINT + || STRLCAT(NameBuff, ext, sizeof(NameBuff)) >= MAXPATHL) { + emsg(_(e_fnametoolong)); + return; + } + + // Note: We cannot just do `&NameBuff` because it is a statically sized array + // so `NameBuff == &NameBuff` according to C semantics. + char *buff_list[1] = { (char *)NameBuff }; + const int res = gen_expand_wildcards(1, buff_list, &filecount, &files, + EW_FILE|EW_SILENT); + if (res == FAIL || filecount == 0) { + if (!got_int) { + semsg(_("E151: No match: %s"), NameBuff); + } + if (res != FAIL) { + FreeWild(filecount, files); + } + return; + } + + // Open the tags file for writing. + // Do this before scanning through all the files. + memcpy(NameBuff, dir, dirlen + 1); + if (!add_pathsep((char *)NameBuff) + || STRLCAT(NameBuff, tagfname, sizeof(NameBuff)) >= MAXPATHL) { + emsg(_(e_fnametoolong)); + return; + } + + FILE *const fd_tags = os_fopen((char *)NameBuff, "w"); + if (fd_tags == NULL) { + if (!ignore_writeerr) { + semsg(_("E152: Cannot open %s for writing"), NameBuff); + } + FreeWild(filecount, files); + return; + } + + // If using the "++t" argument or generating tags for "$VIMRUNTIME/doc" + // add the "help-tags" tag. + ga_init(&ga, (int)sizeof(char_u *), 100); + if (add_help_tags + || path_full_compare("$VIMRUNTIME/doc", dir, false, true) == kEqualFiles) { + size_t s_len = 18 + STRLEN(tagfname); + s = xmalloc(s_len); + snprintf(s, s_len, "help-tags\t%s\t1\n", tagfname); + GA_APPEND(char *, &ga, s); + } + + // Go over all the files and extract the tags. + for (int fi = 0; fi < filecount && !got_int; fi++) { + FILE *const fd = os_fopen(files[fi], "r"); + if (fd == NULL) { + semsg(_("E153: Unable to open %s for reading"), files[fi]); + continue; + } + const char *const fname = files[fi] + dirlen + 1; + + bool firstline = true; + while (!vim_fgets(IObuff, IOSIZE, fd) && !got_int) { + if (firstline) { + // Detect utf-8 file by a non-ASCII char in the first line. + TriState this_utf8 = kNone; + for (s = (char *)IObuff; *s != NUL; s++) { + if ((char_u)(*s) >= 0x80) { + this_utf8 = kTrue; + const int l = utf_ptr2len(s); + if (l == 1) { + // Illegal UTF-8 byte sequence. + this_utf8 = kFalse; + break; + } + s += l - 1; + } + } + if (this_utf8 == kNone) { // only ASCII characters found + this_utf8 = kFalse; + } + if (utf8 == kNone) { // first file + utf8 = this_utf8; + } else if (utf8 != this_utf8) { + semsg(_("E670: Mix of help file encodings within a language: %s"), + files[fi]); + mix = !got_int; + got_int = true; + } + firstline = false; + } + p1 = vim_strchr((char *)IObuff, '*'); // find first '*' + while (p1 != NULL) { + p2 = strchr((const char *)p1 + 1, '*'); // Find second '*'. + if (p2 != NULL && p2 > p1 + 1) { // Skip "*" and "**". + for (s = p1 + 1; s < p2; s++) { + if (*s == ' ' || *s == '\t' || *s == '|') { + break; + } + } + + // Only accept a *tag* when it consists of valid + // characters, there is white space before it and is + // followed by a white character or end-of-line. + if (s == p2 + && ((char_u *)p1 == IObuff || p1[-1] == ' ' || p1[-1] == '\t') + && (vim_strchr(" \t\n\r", s[1]) != NULL + || s[1] == '\0')) { + *p2 = '\0'; + p1++; + size_t s_len= (size_t)(p2 - p1) + STRLEN(fname) + 2; + s = xmalloc(s_len); + GA_APPEND(char *, &ga, s); + snprintf(s, s_len, "%s\t%s", p1, fname); + + // find next '*' + p2 = vim_strchr(p2 + 1, '*'); + } + } + p1 = p2; + } + line_breakcheck(); + } + + fclose(fd); + } + + FreeWild(filecount, files); + + if (!got_int && ga.ga_data != NULL) { + // Sort the tags. + sort_strings(ga.ga_data, ga.ga_len); + + // Check for duplicates. + for (int i = 1; i < ga.ga_len; i++) { + p1 = ((char **)ga.ga_data)[i - 1]; + p2 = ((char **)ga.ga_data)[i]; + while (*p1 == *p2) { + if (*p2 == '\t') { + *p2 = NUL; + vim_snprintf((char *)NameBuff, MAXPATHL, + _("E154: Duplicate tag \"%s\" in file %s/%s"), + ((char_u **)ga.ga_data)[i], dir, p2 + 1); + emsg((char *)NameBuff); + *p2 = '\t'; + break; + } + p1++; + p2++; + } + } + + if (utf8 == kTrue) { + fprintf(fd_tags, "!_TAG_FILE_ENCODING\tutf-8\t//\n"); + } + + // Write the tags into the file. + for (int i = 0; i < ga.ga_len; i++) { + s = ((char **)ga.ga_data)[i]; + if (STRNCMP(s, "help-tags\t", 10) == 0) { + // help-tags entry was added in formatted form + fputs(s, fd_tags); + } else { + fprintf(fd_tags, "%s\t/" "*", s); + for (p1 = s; *p1 != '\t'; p1++) { + // insert backslash before '\\' and '/' + if (*p1 == '\\' || *p1 == '/') { + putc('\\', fd_tags); + } + putc(*p1, fd_tags); + } + fprintf(fd_tags, "*\n"); + } + } + } + if (mix) { + got_int = false; // continue with other languages + } + + GA_DEEP_CLEAR_PTR(&ga); + fclose(fd_tags); // there is no check for an error... +} + +/// Generate tags in one help directory, taking care of translations. +static void do_helptags(char *dirname, bool add_help_tags, bool ignore_writeerr) + FUNC_ATTR_NONNULL_ALL +{ + int len; + garray_T ga; + char lang[2]; + char ext[5]; + char fname[8]; + int filecount; + char **files; + + // Get a list of all files in the help directory and in subdirectories. + STRLCPY(NameBuff, dirname, sizeof(NameBuff)); + if (!add_pathsep((char *)NameBuff) + || STRLCAT(NameBuff, "**", sizeof(NameBuff)) >= MAXPATHL) { + emsg(_(e_fnametoolong)); + return; + } + + // Note: We cannot just do `&NameBuff` because it is a statically sized array + // so `NameBuff == &NameBuff` according to C semantics. + char *buff_list[1] = { (char *)NameBuff }; + if (gen_expand_wildcards(1, buff_list, &filecount, &files, + EW_FILE|EW_SILENT) == FAIL + || filecount == 0) { + semsg(_("E151: No match: %s"), NameBuff); + return; + } + + // Go over all files in the directory to find out what languages are + // present. + int j; + ga_init(&ga, 1, 10); + for (int i = 0; i < filecount; i++) { + len = (int)STRLEN(files[i]); + if (len <= 4) { + continue; + } + + if (STRICMP(files[i] + len - 4, ".txt") == 0) { + // ".txt" -> language "en" + lang[0] = 'e'; + lang[1] = 'n'; + } else if (files[i][len - 4] == '.' + && ASCII_ISALPHA(files[i][len - 3]) + && ASCII_ISALPHA(files[i][len - 2]) + && TOLOWER_ASC(files[i][len - 1]) == 'x') { + // ".abx" -> language "ab" + lang[0] = (char)TOLOWER_ASC(files[i][len - 3]); + lang[1] = (char)TOLOWER_ASC(files[i][len - 2]); + } else { + continue; + } + + // Did we find this language already? + for (j = 0; j < ga.ga_len; j += 2) { + if (STRNCMP(lang, ((char_u *)ga.ga_data) + j, 2) == 0) { + break; + } + } + if (j == ga.ga_len) { + // New language, add it. + ga_grow(&ga, 2); + ((char *)ga.ga_data)[ga.ga_len++] = lang[0]; + ((char *)ga.ga_data)[ga.ga_len++] = lang[1]; + } + } + + // Loop over the found languages to generate a tags file for each one. + for (j = 0; j < ga.ga_len; j += 2) { + STRCPY(fname, "tags-xx"); + fname[5] = ((char *)ga.ga_data)[j]; + fname[6] = ((char *)ga.ga_data)[j + 1]; + if (fname[5] == 'e' && fname[6] == 'n') { + // English is an exception: use ".txt" and "tags". + fname[4] = NUL; + STRCPY(ext, ".txt"); + } else { + // Language "ab" uses ".abx" and "tags-ab". + STRCPY(ext, ".xxx"); + ext[1] = fname[5]; + ext[2] = fname[6]; + } + helptags_one(dirname, (char *)ext, (char *)fname, add_help_tags, ignore_writeerr); + } + + ga_clear(&ga); + FreeWild(filecount, files); +} + +static void helptags_cb(char *fname, void *cookie) + FUNC_ATTR_NONNULL_ALL +{ + do_helptags(fname, *(bool *)cookie, true); +} + +/// ":helptags" +void ex_helptags(exarg_T *eap) +{ + expand_T xpc; + char *dirname; + bool add_help_tags = false; + + // Check for ":helptags ++t {dir}". + if (STRNCMP(eap->arg, "++t", 3) == 0 && ascii_iswhite(eap->arg[3])) { + add_help_tags = true; + eap->arg = skipwhite(eap->arg + 3); + } + + if (STRCMP(eap->arg, "ALL") == 0) { + do_in_path(p_rtp, "doc", DIP_ALL + DIP_DIR, helptags_cb, &add_help_tags); + } else { + ExpandInit(&xpc); + xpc.xp_context = EXPAND_DIRECTORIES; + dirname = (char *)ExpandOne(&xpc, (char_u *)eap->arg, NULL, + WILD_LIST_NOTFOUND|WILD_SILENT, WILD_EXPAND_FREE); + if (dirname == NULL || !os_isdir((char_u *)dirname)) { + semsg(_("E150: Not a directory: %s"), eap->arg); + } else { + do_helptags(dirname, add_help_tags, false); + } + xfree(dirname); + } +} |