changeset 21423:5db63c2c6929 v8.2.1262

patch 8.2.1262: src/ex_cmds.c file is too big Commit: https://github.com/vim/vim/commit/f868ba89039045b25efe83d12ca501d657e170e8 Author: Bram Moolenaar <Bram@vim.org> Date: Tue Jul 21 21:07:20 2020 +0200 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)
author Bram Moolenaar <Bram@vim.org>
date Tue, 21 Jul 2020 21:15:06 +0200
parents af29e2470306
children 8835316c02ba
files Filelist src/Make_cyg_ming.mak src/Make_morph.mak src/Make_mvc.mak src/Make_vms.mms src/Makefile src/README.md src/cmdexpand.c src/ex_cmds.c src/help.c src/proto.h src/proto/ex_cmds.pro src/proto/help.pro src/version.c
diffstat 14 files changed, 1344 insertions(+), 1346 deletions(-) [+]
line wrap: on
line diff
--- a/Filelist
+++ b/Filelist
@@ -68,6 +68,7 @@ SRC_ALL =	\
 		src/gui_beval.c \
 		src/hardcopy.c \
 		src/hashtab.c \
+		src/help.c \
 		src/highlight.c \
 		src/indent.c \
 		src/insexpand.c \
@@ -240,6 +241,7 @@ SRC_ALL =	\
 		src/proto/gui_beval.pro \
 		src/proto/hardcopy.pro \
 		src/proto/hashtab.pro \
+		src/proto/help.pro \
 		src/proto/highlight.pro \
 		src/proto/indent.pro \
 		src/proto/insexpand.pro \
--- a/src/Make_cyg_ming.mak
+++ b/src/Make_cyg_ming.mak
@@ -744,6 +744,7 @@ OBJ = \
 	$(OUTDIR)/gui_xim.o \
 	$(OUTDIR)/hardcopy.o \
 	$(OUTDIR)/hashtab.o \
+	$(OUTDIR)/help.o \
 	$(OUTDIR)/highlight.o \
 	$(OUTDIR)/if_cscope.o \
 	$(OUTDIR)/indent.o \
--- a/src/Make_morph.mak
+++ b/src/Make_morph.mak
@@ -64,6 +64,7 @@ SRC =	arabic.c						\
 	gui_xim.c						\
 	hardcopy.c						\
 	hashtab.c						\
+	help.c							\
 	highlight.c						\
 	indent.c						\
 	insexpand.c						\
--- a/src/Make_mvc.mak
+++ b/src/Make_mvc.mak
@@ -766,6 +766,7 @@ OBJ = \
 	$(OUTDIR)\gui_xim.obj \
 	$(OUTDIR)\hardcopy.obj \
 	$(OUTDIR)\hashtab.obj \
+	$(OUTDIR)\help.obj \
 	$(OUTDIR)\highlight.obj \
 	$(OBJDIR)\if_cscope.obj \
 	$(OUTDIR)\indent.obj \
@@ -1608,6 +1609,8 @@ testclean:
 
 $(OUTDIR)/hashtab.obj:	$(OUTDIR) hashtab.c  $(INCL)
 
+$(OUTDIR)/help.obj:	$(OUTDIR) help.c  $(INCL)
+
 $(OUTDIR)/highlight.obj:	$(OUTDIR) highlight.c  $(INCL)
 
 $(OUTDIR)/indent.obj:	$(OUTDIR) indent.c  $(INCL)
@@ -1930,6 +1933,7 @@ proto.h: \
 	proto/gui_xim.pro \
 	proto/hardcopy.pro \
 	proto/hashtab.pro \
+	proto/help.pro \
 	proto/highlight.pro \
 	proto/indent.pro \
 	proto/insexpand.pro \
--- a/src/Make_vms.mms
+++ b/src/Make_vms.mms
@@ -337,6 +337,7 @@ SRC = \
 	gui_xim.c \
 	hardcopy.c \
 	hashtab.c \
+	help.c \
 	highlight.c \
 	if_cscope.c \
 	if_xcmdsrv.c \
@@ -450,6 +451,7 @@ OBJ = \
 	gui_xim.obj \
 	hardcopy.obj \
 	hashtab.obj \
+	help.obj \
 	highlight.obj \
 	if_cscope.obj \
 	if_mzsch.obj \
@@ -834,6 +836,10 @@ hashtab.obj : hashtab.c vim.h [.auto]con
  ascii.h keymap.h term.h macros.h structs.h regexp.h \
  gui.h beval.h [.proto]gui_beval.pro option.h ex_cmds.h proto.h \
  globals.h
+help.obj : help.c vim.h [.auto]config.h feature.h os_unix.h \
+ ascii.h keymap.h term.h macros.h structs.h regexp.h \
+ gui.h beval.h [.proto]gui_beval.pro option.h ex_cmds.h proto.h \
+ globals.h
 highlight.obj : highlight.c vim.h [.auto]config.h feature.h os_unix.h \
  ascii.h keymap.h term.h macros.h structs.h regexp.h \
  gui.h beval.h [.proto]gui_beval.pro option.h ex_cmds.h proto.h \
--- a/src/Makefile
+++ b/src/Makefile
@@ -404,7 +404,7 @@ CClink = $(CC)
 # Use --with-luajit if you want to use LuaJIT instead of Lua.
 # Set PATH environment variable to find lua or luajit executable.
 # This requires at least "normal" features, "tiny" and "small" don't work.
-#CONF_OPT_LUA = --enable-luainterp
+CONF_OPT_LUA = --enable-luainterp
 #CONF_OPT_LUA = --enable-luainterp=dynamic
 #CONF_OPT_LUA = --enable-luainterp --with-luajit
 #CONF_OPT_LUA = --enable-luainterp=dynamic --with-luajit
@@ -447,10 +447,10 @@ CClink = $(CC)
 # dlopen(), dlsym(), dlclose(), i.e. pythonX.Y.so must be available
 # However, this may still cause problems, such as "import termios" failing.
 # Build two separate versions of Vim in that case.
-#CONF_OPT_PYTHON = --enable-pythoninterp
+CONF_OPT_PYTHON = --enable-pythoninterp
 #CONF_OPT_PYTHON = --enable-pythoninterp --with-python-command=python2.7
 #CONF_OPT_PYTHON = --enable-pythoninterp=dynamic
-#CONF_OPT_PYTHON3 = --enable-python3interp
+CONF_OPT_PYTHON3 = --enable-python3interp
 #CONF_OPT_PYTHON3 = --enable-python3interp --with-python3-command=python3.6
 #CONF_OPT_PYTHON3 = --enable-python3interp=dynamic
 
@@ -472,7 +472,7 @@ CClink = $(CC)
 
 # CSCOPE
 # Uncomment this when you want to include the Cscope interface.
-#CONF_OPT_CSCOPE = --enable-cscope
+CONF_OPT_CSCOPE = --enable-cscope
 
 # NETBEANS - NetBeans interface. Only works with Motif, GTK, and gnome.
 # Motif version must have XPM libraries (see |netbeans-xpm|).
@@ -540,7 +540,7 @@ CClink = $(CC)
 #CONF_OPT_FEAT = --with-features=small
 #CONF_OPT_FEAT = --with-features=normal
 #CONF_OPT_FEAT = --with-features=big
-#CONF_OPT_FEAT = --with-features=huge
+CONF_OPT_FEAT = --with-features=huge
 
 # COMPILED BY - For including a specific e-mail address for ":version".
 #CONF_OPT_COMPBY = "--with-compiledby=John Doe <JohnDoe@yahoo.com>"
@@ -614,7 +614,7 @@ CClink = $(CC)
 # Use this with GCC to check for mistakes, unused arguments, etc.
 # Note: If you use -Wextra and get warnings in GTK code about function
 #       parameters, you can add -Wno-cast-function-type
-#CFLAGS = -g -Wall -Wextra -Wshadow -Wmissing-prototypes -Wunreachable-code -Wno-cast-function-type -Wno-deprecated-declarations -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1
+CFLAGS = -g -Wall -Wextra -Wshadow -Wmissing-prototypes -Wunreachable-code -Wno-cast-function-type -Wno-deprecated-declarations -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1
 # Add -Wpedantic to find // comments and other C99 constructs.
 # Better disable Perl and Python to avoid a lot of warnings.
 #CFLAGS = -g -Wall -Wextra -Wshadow -Wmissing-prototypes -Wpedantic -Wunreachable-code -Wunused-result -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1
@@ -709,7 +709,7 @@ SANITIZER_LIBS = $(SANITIZER_CFLAGS)
 # Configuration is in the .ccmalloc or ~/.ccmalloc file.
 # Doesn't work very well, since memory linked to from global variables
 # (in libraries) is also marked as leaked memory.
-#LEAK_CFLAGS = -DEXITFREE
+LEAK_CFLAGS = -DEXITFREE
 #LEAK_LIBS = -lccmalloc
 
 # Uncomment this line to have Vim call abort() when an internal error is
@@ -1639,6 +1639,7 @@ BASIC_SRC = \
 	gui_xim.c \
 	hardcopy.c \
 	hashtab.c \
+	help.c \
 	highlight.c \
 	if_cscope.c \
 	if_xcmdsrv.c \
@@ -1790,6 +1791,7 @@ OBJ_COMMON = \
 	objects/gui_xim.o \
 	objects/hardcopy.o \
 	objects/hashtab.o \
+	objects/help.o \
 	objects/highlight.o \
 	objects/if_cscope.o \
 	objects/if_xcmdsrv.o \
@@ -1958,6 +1960,7 @@ PRO_AUTO = \
 	gui_beval.pro \
 	hardcopy.pro \
 	hashtab.pro \
+	help.pro \
 	highlight.pro \
 	if_cscope.pro \
 	if_lua.pro \
@@ -3264,6 +3267,9 @@ objects/hardcopy.o: hardcopy.c
 objects/hashtab.o: hashtab.c
 	$(CCC) -o $@ hashtab.c
 
+objects/help.o: help.c
+	$(CCC) -o $@ help.c
+
 objects/gui.o: gui.c
 	$(CCC) -o $@ gui.c
 
@@ -3930,6 +3936,10 @@ objects/hashtab.o: hashtab.c vim.h proto
  auto/osdef.h ascii.h keymap.h term.h macros.h option.h beval.h \
  proto/gui_beval.pro structs.h regexp.h gui.h alloc.h ex_cmds.h spell.h \
  proto.h globals.h
+objects/help.o: help.c vim.h protodef.h auto/config.h feature.h os_unix.h \
+ auto/osdef.h ascii.h keymap.h term.h macros.h option.h beval.h \
+ proto/gui_beval.pro structs.h regexp.h gui.h alloc.h ex_cmds.h spell.h \
+ proto.h globals.h
 objects/highlight.o: highlight.c vim.h protodef.h auto/config.h feature.h \
  os_unix.h auto/osdef.h ascii.h keymap.h term.h macros.h option.h beval.h \
  proto/gui_beval.pro structs.h regexp.h gui.h alloc.h ex_cmds.h spell.h \
--- a/src/README.md
+++ b/src/README.md
@@ -48,6 +48,7 @@ filepath.c	| dealing with file names and
 findfile.c	| search for files in 'path'
 fold.c		| folding
 getchar.c	| getting characters and key mapping
+help.c		| vim help related functions
 highlight.c	| syntax highlighting
 indent.c	| text indentation
 insexpand.c	| Insert mode completion
--- a/src/cmdexpand.c
+++ b/src/cmdexpand.c
@@ -1887,62 +1887,6 @@ expand_cmdline(
     return EXPAND_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.
- */
-    static 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
-
 /*
  * Function given to ExpandGeneric() to obtain the possible arguments of the
  * ":behave {mswin,xterm}" command.
--- a/src/ex_cmds.c
+++ b/src/ex_cmds.c
@@ -23,8 +23,6 @@ static void do_filter(linenr_T line1, li
 static int not_writing(void);
 static int check_readonly(int *forceit, buf_T *buf);
 static void delbuf_msg(char_u *name);
-static int help_compare(const void *s1, const void *s2);
-static void prepare_help_buffer(void);
 
 /*
  * ":ascii" and "ga".
@@ -5035,1278 +5033,6 @@ prepare_tagpreview(
 
 #endif
 
-
-/*
- * ":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;
-}
-
-/*
- * Called when starting to edit a buffer for a help file.
- */
-    static 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);
-    }
-}
-
 /*
  * Make the user happy.
  */
new file mode 100644
--- /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);
+    }
+}
--- a/src/proto.h
+++ b/src/proto.h
@@ -95,6 +95,7 @@ extern int _stricoll(char *a, char *b);
 # include "gui_xim.pro"
 # include "hardcopy.pro"
 # include "hashtab.pro"
+# include "help.pro"
 # include "highlight.pro"
 # include "indent.pro"
 # include "insexpand.pro"
--- a/src/proto/ex_cmds.pro
+++ b/src/proto/ex_cmds.pro
@@ -35,15 +35,6 @@ char_u *get_old_sub(void);
 void set_old_sub(char_u *val);
 void free_old_sub(void);
 int prepare_tagpreview(int undo_sync, int use_previewpopup, use_popup_T use_popup);
-void ex_help(exarg_T *eap);
-void ex_helpclose(exarg_T *eap);
-char_u *check_help_lang(char_u *arg);
-int help_heuristic(char_u *matched_string, int offset, int wrong_case);
-int find_help_tags(char_u *arg, int *num_matches, char_u ***matches, int keep_lang);
-void fix_help_buffer(void);
-void ex_exusage(exarg_T *eap);
-void ex_viusage(exarg_T *eap);
-void ex_helptags(exarg_T *eap);
 void ex_smile(exarg_T *eap);
 void ex_drop(exarg_T *eap);
 char_u *skip_vimgrep_pat(char_u *p, char_u **s, int *flags);
new file mode 100644
--- /dev/null
+++ b/src/proto/help.pro
@@ -0,0 +1,14 @@
+/* help.c */
+void ex_help(exarg_T *eap);
+void ex_helpclose(exarg_T *eap);
+char_u *check_help_lang(char_u *arg);
+int help_heuristic(char_u *matched_string, int offset, int wrong_case);
+int find_help_tags(char_u *arg, int *num_matches, char_u ***matches, int keep_lang);
+void cleanup_help_tags(int num_file, char_u **file);
+void prepare_help_buffer(void);
+void fix_help_buffer(void);
+void ex_exusage(exarg_T *eap);
+void ex_viusage(exarg_T *eap);
+void ex_helptags(exarg_T *eap);
+/* vim: set ft=c : */
+
--- a/src/version.c
+++ b/src/version.c
@@ -755,6 +755,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1262,
+/**/
     1261,
 /**/
     1260,