patch 8.2.1262: src/ex_cmds.c file is too big

Problem:    src/ex_cmds.c file is too big.
Solution:   Move help related code to src/help.c. (Yegappan Lakshmanan,
            closes #6506)
diff --git a/src/help.c b/src/help.c
new file mode 100644
index 0000000..4f2b6b6
--- /dev/null
+++ b/src/help.c
@@ -0,0 +1,1295 @@
+/* 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 (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 = (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(_("E661: Sorry, no '%s' help for %s"), lang, arg);
+	else
+#endif
+	    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 = 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.tab != 0)
+    {
+	if (cmdmod.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.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.keepalt)
+		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.keepalt)
+	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;
+    static char *(mtable[]) = {"*", "g*", "[*", "]*", ":*",
+			       "/*", "/\\*", "\"*", "**",
+			       "cpo-*", "/\\(\\)", "/\\%(\\)",
+			       "?", ":?", "?<CR>", "g?", "g?g?", "g??",
+			       "-?", "q?", "v_g?",
+			       "/\\?", "/\\z(\\)", "\\=", ":s\\=",
+			       "[count]", "[quotex]",
+			       "[range]", ":[range]",
+			       "[pattern]", "\\|", "\\%$",
+			       "s/\\~", "s/\\U", "s/\\L",
+			       "s/\\1", "s/\\2", "s/\\3", "s/\\9"};
+    static char *(rtable[]) = {"star", "gstar", "[star", "]star", ":star",
+			       "/star", "/\\\\star", "quotestar", "starstar",
+			       "cpo-star", "/\\\\(\\\\)", "/\\\\%(\\\\)",
+			       "?", ":?", "?<CR>", "g?", "g?g?", "g??",
+			       "-?", "q?", "v_g?",
+			       "/\\\\?", "/\\\\z(\\\\)", "\\\\=", ":s\\\\=",
+			       "\\[count]", "\\[quotex]",
+			       "\\[range]", ":\\[range]",
+			       "\\[pattern]", "\\\\bar", "/\\\\%\\$",
+			       "s/\\\\\\~", "s/\\\\U", "s/\\\\L",
+			       "s/\\\\1", "s/\\\\2", "s/\\\\3", "s/\\\\9"};
+    static char *(expr_table[]) = {"!=?", "!~?", "<=?", "<?", "==?", "=~?",
+				">=?", ">?", "is?", "isnot?"};
+    int flags;
+
+    d = IObuff;		    // assume IObuff is long enough!
+
+    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)(sizeof(expr_table) / sizeof(char *)); --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
+	// '*' with "star".  Otherwise '*' is recognized as a wildcard.
+	for (i = (int)(sizeof(mtable) / sizeof(char *)); --i >= 0; )
+	    if (STRCMP(arg, mtable[i]) == 0)
+	    {
+		STRCPY(d, rtable[i]);
+		break;
+	    }
+    }
+
+    if (i < 0)	// 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)))
+	{
+	    STRCPY(d, "/\\\\");
+	    STRCPY(d + 3, 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 < ' ')
+		{
+#ifdef EBCDIC
+		    *d++ = CtrlChar(*s);
+#else
+		    *d++ = *s + '@';
+#endif
+		    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.
+    // 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 =
+#ifdef EBCDIC
+	    (char_u *)"65-255,^*,^|,^\"";
+#else
+	    (char_u *)"!-~,^*,^|,^\",192-255";
+#endif
+    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((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)
+			{
+			    for (i2 = 0; i2 < fcount; ++i2)
+			    {
+				if (i1 == i2)
+				    continue;
+				if (fnames[i1] == NULL || fnames[i2] == NULL)
+				    continue;
+				f1 = fnames[i1];
+				f2 = fnames[i2];
+				t1 = gettail(f1);
+				t2 = gettail(f2);
+				e1 = vim_strrchr(t1, '.');
+				e2 = vim_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.
+				    VIM_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
+				    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		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		mix = FALSE;	// detected mixed encodings
+
+    // Find all *.txt files.
+    dirlen = (int)STRLEN(dir);
+    STRCPY(NameBuff, dir);
+    STRCAT(NameBuff, "/**/*");
+    STRCAT(NameBuff, ext);
+    if (gen_expand_wildcards(1, &NameBuff, &filecount, &files,
+						    EW_FILE|EW_SILENT) == FAIL
+	    || filecount == 0)
+    {
+	if (!got_int)
+	    semsg(_("E151: No match: %s"), NameBuff);
+	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(_("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_init2(&ga, (int)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(_("E153: Unable to open %s for reading"), files[fi]);
+	    continue;
+	}
+	fname = files[fi] + dirlen + 1;
+
+	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(_("E670: Mix of help file encodings within a language: %s"), files[fi]);
+		    mix = !got_int;
+		    got_int = TRUE;
+		}
+		firstline = 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;
+	    }
+	    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,
+			    _("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 == 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(_("E151: No match: %s"), 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)
+	{
+	    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(_("E150: Not a directory: %s"), eap->arg);
+	else
+	    do_helptags(dirname, add_help_tags, FALSE);
+	vim_free(dirname);
+    }
+}