summaryrefslogtreecommitdiffstats
path: root/src/help.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 20:09:20 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 20:09:20 +0000
commit029f72b1a93430b24b88eb3a72c6114d9f149737 (patch)
tree765d5c2041967f9c6fef195fe343d9234a030e90 /src/help.c
parentInitial commit. (diff)
downloadvim-029f72b1a93430b24b88eb3a72c6114d9f149737.tar.xz
vim-029f72b1a93430b24b88eb3a72c6114d9f149737.zip
Adding upstream version 2:9.1.0016.upstream/2%9.1.0016
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/help.c')
-rw-r--r--src/help.c1330
1 files changed, 1330 insertions, 0 deletions
diff --git a/src/help.c b/src/help.c
new file mode 100644
index 0000000..c33985e
--- /dev/null
+++ b/src/help.c
@@ -0,0 +1,1330 @@
+/* vi:set ts=8 sts=4 sw=4 noet:
+ *
+ * VIM - Vi IMproved by Bram Moolenaar
+ *
+ * Do ":help uganda" in Vim to read copying and usage conditions.
+ * Do ":help credits" in Vim to see a list of people who contributed.
+ * See README.txt for an overview of the Vim source code.
+ */
+
+/*
+ * help.c: functions for Vim help
+ */
+
+#include "vim.h"
+
+/*
+ * ":help": open a read-only window on a help file
+ */
+ void
+ex_help(exarg_T *eap)
+{
+ char_u *arg;
+ char_u *tag;
+ FILE *helpfd; // file descriptor of help file
+ int n;
+ int i;
+ win_T *wp;
+ int num_matches;
+ char_u **matches;
+ char_u *p;
+ int empty_fnum = 0;
+ int alt_fnum = 0;
+ buf_T *buf;
+#ifdef FEAT_MULTI_LANG
+ int len;
+ char_u *lang;
+#endif
+#ifdef FEAT_FOLDING
+ int old_KeyTyped = KeyTyped;
+#endif
+
+ if (ERROR_IF_ANY_POPUP_WINDOW)
+ return;
+
+ 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(_(e_dont_panic));
+ return;
+ }
+
+ if (eap->skip) // not executing commands
+ return;
+ }
+ else
+ arg = (char_u *)"";
+
+ // remove trailing blanks
+ p = arg + STRLEN(arg) - 1;
+ while (p > arg && VIM_ISWHITE(*p) && p[-1] != '\\')
+ *p-- = NUL;
+
+#ifdef FEAT_MULTI_LANG
+ // Check for a specified language
+ lang = check_help_lang(arg);
+#endif
+
+ // When no argument given go to the index.
+ if (*arg == NUL)
+ arg = (char_u *)"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;
+#ifdef FEAT_MULTI_LANG
+ 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;
+ }
+#endif
+ if (i >= num_matches || n == FAIL)
+ {
+#ifdef FEAT_MULTI_LANG
+ if (lang != NULL)
+ semsg(_(e_sorry_no_str_help_for_str), lang, arg);
+ else
+#endif
+ semsg(_(e_sorry_no_help_for_str), arg);
+ if (n != FAIL)
+ FreeWild(num_matches, matches);
+ return;
+ }
+
+ // The first match (in the requested language) is the best match.
+ tag = vim_strsave(matches[i]);
+ FreeWild(num_matches, matches);
+
+#ifdef FEAT_GUI
+ need_mouse_correct = TRUE;
+#endif
+
+ // 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
+ FOR_ALL_WINDOWS(wp)
+ if (bt_help(wp->w_buffer))
+ 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 = mch_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 |= p_sb ? WSP_BOT : 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;
+ }
+ }
+
+ if (!p_im)
+ restart_edit = 0; // don't want insert mode in help file
+
+#ifdef FEAT_FOLDING
+ // Restore KeyTyped, setting 'filetype=help' may reset it.
+ // It is needed for do_tag top open folds under the cursor.
+ KeyTyped = old_KeyTyped;
+#endif
+
+ if (tag != NULL)
+ do_tag(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:
+ vim_free(tag);
+}
+
+/*
+ * ":helpclose": Close one help window
+ */
+ void
+ex_helpclose(exarg_T *eap UNUSED)
+{
+ win_T *win;
+
+ FOR_ALL_WINDOWS(win)
+ {
+ if (bt_help(win->w_buffer))
+ {
+ win_close(win, FALSE);
+ return;
+ }
+ }
+}
+
+#if defined(FEAT_MULTI_LANG) || defined(PROTO)
+/*
+ * In an argument search for a language specifiers in the form "@xx".
+ * Changes the "@" to NUL if found, and returns a pointer to "xx".
+ * Returns NULL if not found.
+ */
+ char_u *
+check_help_lang(char_u *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;
+}
+#endif
+
+/*
+ * 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.
+ */
+ int
+help_heuristic(
+ char_u *matched_string,
+ int offset, // offset for match
+ int wrong_case) // no matching case
+{
+ int num_letters;
+ char_u *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 (ASCII_ISALNUM(matched_string[offset]) && offset > 0
+ && 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 (int)(100 * num_letters + 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;
+ int cmp;
+
+ p1 = *(char **)s1 + strlen(*(char **)s1) + 1;
+ p2 = *(char **)s2 + strlen(*(char **)s2) + 1;
+
+ // Compare by help heuristic number first.
+ 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(
+ char_u *arg,
+ int *num_matches,
+ char_u ***matches,
+ int keep_lang)
+{
+ char_u *s, *d;
+ 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"},
+ {"/\\*", "/\\\\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 char *(expr_table[]) = {"!=?", "!~?", "<=?", "<?", "==?", "=~?",
+ ">=?", ">?", "is?", "isnot?"};
+ int flags;
+
+ d = 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_LENGTH(expr_table); --i >= 0; )
+ if (STRCMP(arg + 5, expr_table[i]) == 0)
+ {
+ int si = 0, di = 0;
+
+ for (;;)
+ {
+ if (arg[si] == '~')
+ d[di++] = '\\';
+ d[di++] = arg[si];
+ if (arg[si] == NUL)
+ break;
+ ++si;
+ }
+ 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((char_u *)"%_z@", arg[1]) != NULL
+ && arg[2] != NUL)))
+ {
+ vim_snprintf((char *)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 (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 (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((char_u *)"?@[\\]^", s[1]) != NULL)))
+ {
+ if (d > IObuff && d[-1] != '_' && d[-1] != '\\')
+ *d++ = '_'; // prepend a '_' to make x_CTRL-x
+ STRCPY(d, "CTRL-");
+ d += 5;
+ if (*s < ' ')
+ {
+ *d++ = *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++ = '\\';
+
+ // Insert a backslash before a backslash after a slash, for search
+ // pattern tags: "/\|" --> "/\\|".
+ else if (s[0] == '\\' && s[1] != '\\'
+ && *arg == '/' && s == arg + 1)
+ *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 '}'.
+ if (*s == '}' && s > arg && *arg == '{')
+ break;
+ }
+ *d = NUL;
+
+ if (*IObuff == '`')
+ {
+ if (d > IObuff + 2 && d[-1] == '`')
+ {
+ // remove the backticks from `command`
+ mch_memmove(IObuff, IObuff + 1, STRLEN(IObuff));
+ d[-2] = NUL;
+ }
+ else if (d > IObuff + 3 && d[-2] == '`' && d[-1] == ',')
+ {
+ // remove the backticks and comma from `command`,
+ mch_memmove(IObuff, IObuff + 1, STRLEN(IObuff));
+ d[-3] = NUL;
+ }
+ else if (d > IObuff + 4 && d[-3] == '`'
+ && d[-2] == '\\' && d[-1] == '.')
+ {
+ // remove the backticks and dot from `command`\.
+ mch_memmove(IObuff, IObuff + 1, STRLEN(IObuff));
+ d[-4] = NUL;
+ }
+ }
+ }
+ }
+
+ *matches = (char_u **)"";
+ *num_matches = 0;
+ 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, (int)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)
+ vim_free((*matches)[--*num_matches]);
+ }
+ return OK;
+}
+
+#ifdef FEAT_MULTI_LANG
+/*
+ * 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_u **file)
+{
+ int i, j;
+ int len;
+ 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 (i = 0; i < num_file; ++i)
+ {
+ 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".
+ 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 (i = 0; i < num_file; ++i)
+ {
+ 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;
+ }
+ }
+}
+#endif
+
+/*
+ * Called when starting to edit a buffer for a help file.
+ */
+ void
+prepare_help_buffer(void)
+{
+ char_u *p;
+
+ curbuf->b_help = TRUE;
+#ifdef FEAT_QUICKFIX
+ set_string_option_direct((char_u *)"buftype", -1,
+ (char_u *)"help", OPT_FREE|OPT_LOCAL, 0);
+#endif
+
+ // Always set these options after jumping to a help tag, because the
+ // user may have an autocommand that gets in the way.
+ // When adding an option here, also update the help file helphelp.txt.
+
+ // 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.
+ p = (char_u *)"!-~,^*,^|,^\",192-255";
+ if (STRCMP(curbuf->b_p_isk, p) != 0)
+ {
+ set_string_option_direct((char_u *)"isk", -1, p, OPT_FREE|OPT_LOCAL, 0);
+ check_buf_options(curbuf);
+ (void)buf_init_chartab(curbuf, FALSE);
+ }
+
+#ifdef FEAT_FOLDING
+ // Don't use the global foldmethod.
+ set_string_option_direct((char_u *)"fdm", -1, (char_u *)"manual",
+ OPT_FREE|OPT_LOCAL, 0);
+#endif
+
+ 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
+#ifdef FEAT_ARABIC
+ curwin->w_p_arab = FALSE; // no arabic mode
+#endif
+#ifdef FEAT_RIGHTLEFT
+ curwin->w_p_rl = FALSE; // help window is left-to-right
+#endif
+#ifdef FEAT_FOLDING
+ curwin->w_p_fen = FALSE; // No folding in the help window
+#endif
+#ifdef FEAT_DIFF
+ curwin->w_p_diff = FALSE; // No 'diff'
+#endif
+#ifdef FEAT_SPELL
+ curwin->w_p_spell = FALSE; // No spell checking
+#endif
+
+ 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_u *line;
+ int in_example = FALSE;
+ int len;
+ char_u *fname;
+ char_u *p;
+ char_u *rt;
+ int mustfree;
+
+ // Set filetype to "help" if still needed.
+ if (STRCMP(curbuf->b_p_ft, "help") != 0)
+ {
+ ++curbuf_lock;
+ set_option_value_give_err((char_u *)"ft",
+ 0L, (char_u *)"help", OPT_LOCAL);
+ --curbuf_lock;
+ }
+
+#ifdef FEAT_SYN_HL
+ if (!syntax_present(curwin))
+#endif
+ {
+ for (lnum = 1; lnum <= curbuf->b_ml.ml_line_count; ++lnum)
+ {
+ line = ml_get_buf(curbuf, lnum, FALSE);
+ len = (int)STRLEN(line);
+ if (in_example && len > 0 && !VIM_ISWHITE(line[0]))
+ {
+ // End of example: non-white or '<' in first column.
+ if (line[0] == '<')
+ {
+ // blank-out a '<' in the first column
+ line = 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 = 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 = 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.
+ fname = gettail(curbuf->b_fname);
+ if (fnamecmp(fname, "help.txt") == 0
+#ifdef FEAT_MULTI_LANG
+ || (fnamencmp(fname, "help.", 5) == 0
+ && ASCII_ISALPHA(fname[5])
+ && ASCII_ISALPHA(fname[6])
+ && TOLOWER_ASC(fname[7]) == 'x'
+ && fname[8] == NUL)
+#endif
+ )
+ {
+ for (lnum = 1; lnum < curbuf->b_ml.ml_line_count; ++lnum)
+ {
+ line = ml_get_buf(curbuf, lnum, FALSE);
+ if (strstr((char *)line, "*local-additions*") == NULL)
+ continue;
+
+ // Go through all directories in 'runtimepath', skipping
+ // $VIMRUNTIME.
+ p = p_rtp;
+ while (*p != NUL)
+ {
+ copy_option_part(&p, NameBuff, MAXPATHL, ",");
+ mustfree = FALSE;
+ rt = vim_getenv((char_u *)"VIMRUNTIME", &mustfree);
+ if (rt != NULL &&
+ fullpathcmp(rt, NameBuff, FALSE, TRUE) != FPC_SAME)
+ {
+ int fcount;
+ char_u **fnames;
+ FILE *fd;
+ char_u *s;
+ int fi;
+ vimconv_T vc;
+ char_u *cp;
+
+ // Find all "doc/ *.txt" files in this directory.
+ add_pathsep(NameBuff);
+#ifdef FEAT_MULTI_LANG
+ STRCAT(NameBuff, "doc/*.??[tx]");
+#else
+ STRCAT(NameBuff, "doc/*.txt");
+#endif
+ if (gen_expand_wildcards(1, &NameBuff, &fcount,
+ &fnames, EW_FILE|EW_SILENT) == OK
+ && fcount > 0)
+ {
+#ifdef FEAT_MULTI_LANG
+ int i1, i2;
+ char_u *f1, *f2;
+ char_u *t1, *t2;
+ char_u *e1, *e2;
+
+ // If foo.abx is found use it instead of foo.txt in
+ // the same directory.
+ for (i1 = 0; i1 < fcount; ++i1)
+ {
+ f1 = fnames[i1];
+ t1 = gettail(f1);
+ e1 = vim_strrchr(t1, '.');
+ if (fnamecmp(e1, ".txt") != 0
+ && fnamecmp(e1, fname + 4) != 0)
+ {
+ // Not .txt and not .abx, remove it.
+ VIM_CLEAR(fnames[i1]);
+ continue;
+ }
+
+ for (i2 = i1 + 1; i2 < fcount; ++i2)
+ {
+ f2 = fnames[i2];
+ if (f2 == NULL)
+ continue;
+ t2 = gettail(f2);
+ e2 = vim_strrchr(t2, '.');
+ 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
+ VIM_CLEAR(fnames[i1]);
+ }
+ }
+#endif
+ for (fi = 0; fi < fcount; ++fi)
+ {
+ if (fnames[fi] == NULL)
+ continue;
+ fd = mch_fopen((char *)fnames[fi], "r");
+ if (fd != NULL)
+ {
+ vim_fgets(IObuff, IOSIZE, fd);
+ if (IObuff[0] == '*'
+ && (s = vim_strchr(IObuff + 1, '*'))
+ != NULL)
+ {
+ int this_utf = MAYBE;
+
+ // 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 (*s >= 0x80 && this_utf != FALSE)
+ {
+ int l;
+
+ this_utf = TRUE;
+ l = utf_ptr2len(s);
+ if (l == 1)
+ this_utf = FALSE;
+ 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 == TRUE ? "utf-8"
+ : "latin1"), p_enc);
+ if (vc.vc_type == CONV_NONE)
+ // No conversion needed.
+ cp = IObuff;
+ else
+ {
+ // Do the conversion. If it fails
+ // use the unconverted text.
+ cp = string_convert(&vc, IObuff,
+ NULL);
+ if (cp == NULL)
+ cp = IObuff;
+ }
+ convert_setup(&vc, NULL, NULL);
+
+ ml_append(lnum, cp, (colnr_T)0, FALSE);
+ if (cp != IObuff)
+ vim_free(cp);
+ ++lnum;
+ }
+ fclose(fd);
+ }
+ }
+ FreeWild(fcount, fnames);
+ }
+ }
+ if (mustfree)
+ vim_free(rt);
+ }
+ break;
+ }
+ }
+}
+
+/*
+ * ":exusage"
+ */
+ void
+ex_exusage(exarg_T *eap UNUSED)
+{
+ do_cmdline_cmd((char_u *)"help ex-cmd-index");
+}
+
+/*
+ * ":viusage"
+ */
+ void
+ex_viusage(exarg_T *eap UNUSED)
+{
+ do_cmdline_cmd((char_u *)"help normal-index");
+}
+
+/*
+ * Generate tags in one help directory.
+ */
+ static void
+helptags_one(
+ char_u *dir, // doc directory
+ char_u *ext, // suffix, ".txt", ".itx", ".frx", etc.
+ char_u *tagfname, // "tags" for English, "tags-fr" for French.
+ int add_help_tags, // add "help-tags" tag
+ int ignore_writeerr) // ignore write error
+{
+ FILE *fd_tags;
+ FILE *fd;
+ garray_T ga;
+ int res;
+ int filecount;
+ char_u **files;
+ char_u *p1, *p2;
+ int fi;
+ char_u *s;
+ int i;
+ char_u *fname;
+ int dirlen;
+ int utf8 = MAYBE;
+ int this_utf8;
+ int firstline;
+ int in_example;
+ int len;
+ int mix = FALSE; // detected mixed encodings
+
+ // Find all *.txt files.
+ dirlen = (int)STRLEN(dir);
+ STRCPY(NameBuff, dir);
+ STRCAT(NameBuff, "/**/*");
+ STRCAT(NameBuff, ext);
+ res = gen_expand_wildcards(1, &NameBuff, &filecount, &files,
+ EW_FILE|EW_SILENT);
+ if (res == FAIL || filecount == 0)
+ {
+ if (!got_int)
+ semsg(_(e_no_match_str_1), NameBuff);
+ if (res != FAIL)
+ FreeWild(filecount, files);
+ return;
+ }
+
+ // Open the tags file for writing.
+ // Do this before scanning through all the files.
+ STRCPY(NameBuff, dir);
+ add_pathsep(NameBuff);
+ STRCAT(NameBuff, tagfname);
+ fd_tags = mch_fopen((char *)NameBuff, "w");
+ if (fd_tags == NULL)
+ {
+ if (!ignore_writeerr)
+ semsg(_(e_cannot_open_str_for_writing_1), NameBuff);
+ FreeWild(filecount, files);
+ return;
+ }
+
+ // If using the "++t" argument or generating tags for "$VIMRUNTIME/doc"
+ // add the "help-tags" tag.
+ ga_init2(&ga, sizeof(char_u *), 100);
+ if (add_help_tags || fullpathcmp((char_u *)"$VIMRUNTIME/doc",
+ dir, FALSE, TRUE) == FPC_SAME)
+ {
+ if (ga_grow(&ga, 1) == FAIL)
+ got_int = TRUE;
+ else
+ {
+ s = alloc(18 + (unsigned)STRLEN(tagfname));
+ if (s == NULL)
+ got_int = TRUE;
+ else
+ {
+ sprintf((char *)s, "help-tags\t%s\t1\n", tagfname);
+ ((char_u **)ga.ga_data)[ga.ga_len] = s;
+ ++ga.ga_len;
+ }
+ }
+ }
+
+ // Go over all the files and extract the tags.
+ for (fi = 0; fi < filecount && !got_int; ++fi)
+ {
+ fd = mch_fopen((char *)files[fi], "r");
+ if (fd == NULL)
+ {
+ semsg(_(e_unable_to_open_str_for_reading), files[fi]);
+ continue;
+ }
+ fname = files[fi] + dirlen + 1;
+
+ in_example = FALSE;
+ 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.
+ this_utf8 = MAYBE;
+ for (s = IObuff; *s != NUL; ++s)
+ if (*s >= 0x80)
+ {
+ int l;
+
+ this_utf8 = TRUE;
+ l = utf_ptr2len(s);
+ if (l == 1)
+ {
+ // Illegal UTF-8 byte sequence.
+ this_utf8 = FALSE;
+ break;
+ }
+ s += l - 1;
+ }
+ if (this_utf8 == MAYBE) // only ASCII characters found
+ this_utf8 = FALSE;
+ if (utf8 == MAYBE) // first file
+ utf8 = this_utf8;
+ else if (utf8 != this_utf8)
+ {
+ semsg(_(e_mix_of_help_file_encodings_within_language_str), files[fi]);
+ mix = !got_int;
+ got_int = TRUE;
+ }
+ firstline = FALSE;
+ }
+ if (in_example)
+ {
+ // skip over example; a non-white in the first column ends it
+ if (vim_strchr((char_u *)" \t\n\r", IObuff[0]))
+ continue;
+ in_example = FALSE;
+ }
+ p1 = vim_strchr(IObuff, '*'); // find first '*'
+ while (p1 != NULL)
+ {
+ // Use vim_strbyte() instead of vim_strchr() so that when
+ // 'encoding' is dbcs it still works, don't find '*' in the
+ // second byte.
+ p2 = vim_strbyte(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
+ && (p1 == IObuff || p1[-1] == ' ' || p1[-1] == '\t')
+ && (vim_strchr((char_u *)" \t\n\r", s[1]) != NULL
+ || s[1] == '\0'))
+ {
+ *p2 = '\0';
+ ++p1;
+ if (ga_grow(&ga, 1) == FAIL)
+ {
+ got_int = TRUE;
+ break;
+ }
+ s = alloc(p2 - p1 + STRLEN(fname) + 2);
+ if (s == NULL)
+ {
+ got_int = TRUE;
+ break;
+ }
+ ((char_u **)ga.ga_data)[ga.ga_len] = s;
+ ++ga.ga_len;
+ sprintf((char *)s, "%s\t%s", p1, fname);
+
+ // find next '*'
+ p2 = vim_strchr(p2 + 1, '*');
+ }
+ }
+ p1 = p2;
+ }
+ len = (int)STRLEN(IObuff);
+ if ((len == 2 && STRCMP(&IObuff[len - 2], ">\n") == 0)
+ || (len >= 3 && STRCMP(&IObuff[len - 3], " >\n") == 0))
+ in_example = TRUE;
+ line_breakcheck();
+ }
+
+ fclose(fd);
+ }
+
+ FreeWild(filecount, files);
+
+ if (!got_int)
+ {
+ // Sort the tags.
+ if (ga.ga_data != NULL)
+ sort_strings((char_u **)ga.ga_data, ga.ga_len);
+
+ // Check for duplicates.
+ for (i = 1; i < ga.ga_len; ++i)
+ {
+ p1 = ((char_u **)ga.ga_data)[i - 1];
+ p2 = ((char_u **)ga.ga_data)[i];
+ while (*p1 == *p2)
+ {
+ if (*p2 == '\t')
+ {
+ *p2 = NUL;
+ vim_snprintf((char *)NameBuff, MAXPATHL,
+ _(e_duplicate_tag_str_in_file_str_str),
+ ((char_u **)ga.ga_data)[i], dir, p2 + 1);
+ emsg((char *)NameBuff);
+ *p2 = '\t';
+ break;
+ }
+ ++p1;
+ ++p2;
+ }
+ }
+
+ if (utf8 == TRUE)
+ fprintf(fd_tags, "!_TAG_FILE_ENCODING\tutf-8\t//\n");
+
+ // Write the tags into the file.
+ for (i = 0; i < ga.ga_len; ++i)
+ {
+ s = ((char_u **)ga.ga_data)[i];
+ if (STRNCMP(s, "help-tags\t", 10) == 0)
+ // help-tags entry was added in formatted form
+ fputs((char *)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
+
+ for (i = 0; i < ga.ga_len; ++i)
+ vim_free(((char_u **)ga.ga_data)[i]);
+ ga_clear(&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_u *dirname, int add_help_tags, int ignore_writeerr)
+{
+#ifdef FEAT_MULTI_LANG
+ int len;
+ int i, j;
+ garray_T ga;
+ char_u lang[2];
+ char_u ext[5];
+ char_u fname[8];
+ int filecount;
+ char_u **files;
+
+ // Get a list of all files in the help directory and in subdirectories.
+ STRCPY(NameBuff, dirname);
+ add_pathsep(NameBuff);
+ STRCAT(NameBuff, "**");
+ if (gen_expand_wildcards(1, &NameBuff, &filecount, &files,
+ EW_FILE|EW_SILENT) == FAIL
+ || filecount == 0)
+ {
+ semsg(_(e_no_match_str_1), NameBuff);
+ return;
+ }
+
+ // Go over all files in the directory to find out what languages are
+ // present.
+ ga_init2(&ga, 1, 10);
+ for (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] = TOLOWER_ASC(files[i][len - 3]);
+ lang[1] = 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.
+ if (ga_grow(&ga, 2) == FAIL)
+ break;
+ ((char_u *)ga.ga_data)[ga.ga_len++] = lang[0];
+ ((char_u *)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_u *)ga.ga_data)[j];
+ fname[6] = ((char_u *)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, ext, fname, add_help_tags, ignore_writeerr);
+ }
+
+ ga_clear(&ga);
+ FreeWild(filecount, files);
+
+#else
+ // No language support, just use "*.txt" and "tags".
+ helptags_one(dirname, (char_u *)".txt", (char_u *)"tags", add_help_tags,
+ ignore_writeerr);
+#endif
+}
+
+ static void
+helptags_cb(char_u *fname, void *cookie)
+{
+ do_helptags(fname, *(int *)cookie, TRUE);
+}
+
+/*
+ * ":helptags"
+ */
+ void
+ex_helptags(exarg_T *eap)
+{
+ expand_T xpc;
+ char_u *dirname;
+ int add_help_tags = FALSE;
+
+ // Check for ":helptags ++t {dir}".
+ if (STRNCMP(eap->arg, "++t", 3) == 0 && VIM_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, "", (char_u *)"doc", DIP_ALL + DIP_DIR,
+ helptags_cb, &add_help_tags);
+ }
+ else
+ {
+ ExpandInit(&xpc);
+ xpc.xp_context = EXPAND_DIRECTORIES;
+ dirname = ExpandOne(&xpc, eap->arg, NULL,
+ WILD_LIST_NOTFOUND|WILD_SILENT, WILD_EXPAND_FREE);
+ if (dirname == NULL || !mch_isdir(dirname))
+ semsg(_(e_not_a_directory_str), eap->arg);
+ else
+ do_helptags(dirname, add_help_tags, FALSE);
+ vim_free(dirname);
+ }
+}