neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

help.c (23440B)


      1 // help.c: functions for Vim help
      2 
      3 #include <stdbool.h>
      4 #include <stdint.h>
      5 #include <stdio.h>
      6 #include <stdlib.h>
      7 #include <string.h>
      8 
      9 #include "nvim/ascii_defs.h"
     10 #include "nvim/buffer.h"
     11 #include "nvim/buffer_defs.h"
     12 #include "nvim/change.h"
     13 #include "nvim/charset.h"
     14 #include "nvim/cmdexpand.h"
     15 #include "nvim/cmdexpand_defs.h"
     16 #include "nvim/errors.h"
     17 #include "nvim/ex_cmds.h"
     18 #include "nvim/ex_cmds_defs.h"
     19 #include "nvim/ex_docmd.h"
     20 #include "nvim/extmark_defs.h"
     21 #include "nvim/fileio.h"
     22 #include "nvim/garray.h"
     23 #include "nvim/garray_defs.h"
     24 #include "nvim/gettext_defs.h"
     25 #include "nvim/globals.h"
     26 #include "nvim/help.h"
     27 #include "nvim/lua/executor.h"
     28 #include "nvim/macros_defs.h"
     29 #include "nvim/mark.h"
     30 #include "nvim/mbyte.h"
     31 #include "nvim/mbyte_defs.h"
     32 #include "nvim/memline.h"
     33 #include "nvim/memory.h"
     34 #include "nvim/message.h"
     35 #include "nvim/option.h"
     36 #include "nvim/option_defs.h"
     37 #include "nvim/option_vars.h"
     38 #include "nvim/optionstr.h"
     39 #include "nvim/os/fs.h"
     40 #include "nvim/os/input.h"
     41 #include "nvim/os/os.h"
     42 #include "nvim/os/os_defs.h"
     43 #include "nvim/path.h"
     44 #include "nvim/pos_defs.h"
     45 #include "nvim/runtime.h"
     46 #include "nvim/strings.h"
     47 #include "nvim/tag.h"
     48 #include "nvim/types_defs.h"
     49 #include "nvim/vim_defs.h"
     50 #include "nvim/window.h"
     51 
     52 #include "help.c.generated.h"
     53 
     54 /// ":help": open a read-only window on a help file
     55 void ex_help(exarg_T *eap)
     56 {
     57  char *arg;
     58  FILE *helpfd;          // file descriptor of help file
     59  win_T *wp;
     60  int num_matches;
     61  char **matches;
     62  int empty_fnum = 0;
     63  int alt_fnum = 0;
     64  const bool old_KeyTyped = KeyTyped;
     65 
     66  if (eap != NULL) {
     67    // A ":help" command ends at the first LF, or at a '|' that is
     68    // followed by some text.  Set nextcmd to the following command.
     69    for (arg = eap->arg; *arg; arg++) {
     70      if (*arg == '\n' || *arg == '\r'
     71          || (*arg == '|' && arg[1] != NUL && arg[1] != '|')) {
     72        *arg++ = NUL;
     73        eap->nextcmd = arg;
     74        break;
     75      }
     76    }
     77    arg = eap->arg;
     78 
     79    if (eap->forceit && *arg == NUL && !curbuf->b_help) {
     80      emsg(_("E478: Don't panic!"));
     81      return;
     82    }
     83 
     84    if (eap->skip) {        // not executing commands
     85      return;
     86    }
     87  } else {
     88    arg = "";
     89  }
     90 
     91  // remove trailing blanks
     92  char *p = arg + strlen(arg) - 1;
     93  while (p > arg && ascii_iswhite(*p) && p[-1] != '\\') {
     94    *p-- = NUL;
     95  }
     96 
     97  // Check for a specified language
     98  char *lang = check_help_lang(arg);
     99 
    100  // When no argument given go to the index.
    101  if (*arg == NUL) {
    102    arg = "help.txt";
    103  }
    104 
    105  // Check if there is a match for the argument.
    106  int n = find_help_tags(arg, &num_matches, &matches, eap != NULL && eap->forceit);
    107 
    108  int i = 0;
    109  if (n != FAIL && lang != NULL) {
    110    // Find first item with the requested language.
    111    for (i = 0; i < num_matches; i++) {
    112      int len = (int)strlen(matches[i]);
    113      if (len > 3 && matches[i][len - 3] == '@'
    114          && STRICMP(matches[i] + len - 2, lang) == 0) {
    115        break;
    116      }
    117    }
    118  }
    119  if (i >= num_matches || n == FAIL) {
    120    if (lang != NULL) {
    121      semsg(_("E661: Sorry, no '%s' help for %s"), lang, arg);
    122    } else {
    123      semsg(_("E149: Sorry, no help for %s"), arg);
    124    }
    125    if (n != FAIL) {
    126      FreeWild(num_matches, matches);
    127    }
    128    return;
    129  }
    130 
    131  // The first match (in the requested language) is the best match.
    132  char *tag = xstrdup(matches[i]);
    133  FreeWild(num_matches, matches);
    134 
    135  // Re-use an existing help window or open a new one.
    136  // Always open a new one for ":tab help".
    137  if (!bt_help(curwin->w_buffer) || cmdmod.cmod_tab != 0) {
    138    if (cmdmod.cmod_tab != 0) {
    139      wp = NULL;
    140    } else {
    141      wp = NULL;
    142      FOR_ALL_WINDOWS_IN_TAB(wp2, curtab) {
    143        if (bt_help(wp2->w_buffer) && !wp2->w_config.hide && wp2->w_config.focusable) {
    144          wp = wp2;
    145          break;
    146        }
    147      }
    148    }
    149    if (wp != NULL && wp->w_buffer->b_nwindows > 0) {
    150      win_enter(wp, true);
    151    } else {
    152      // There is no help window yet.
    153      // Try to open the file specified by the "helpfile" option.
    154      if ((helpfd = os_fopen(p_hf, READBIN)) == NULL) {
    155        smsg(0, _("Sorry, help file \"%s\" not found"), p_hf);
    156        goto erret;
    157      }
    158      fclose(helpfd);
    159 
    160      // Split off help window; put it at far top if no position
    161      // specified, the current window is vertically split and
    162      // narrow.
    163      n = WSP_HELP;
    164      if (cmdmod.cmod_split == 0 && curwin->w_width != Columns
    165          && curwin->w_width < 80) {
    166        n |= p_sb ? WSP_BOT : WSP_TOP;
    167      }
    168      if (win_split(0, n) == FAIL) {
    169        goto erret;
    170      }
    171 
    172      if (curwin->w_height < p_hh) {
    173        win_setheight((int)p_hh);
    174      }
    175 
    176      // Open help file (do_ecmd() will set b_help flag, readfile() will
    177      // set b_p_ro flag).
    178      // Set the alternate file to the previously edited file.
    179      alt_fnum = curbuf->b_fnum;
    180      do_ecmd(0, NULL, NULL, NULL, ECMD_LASTL,
    181              ECMD_HIDE + ECMD_SET_HELP,
    182              NULL);  // buffer is still open, don't store info
    183 
    184      if ((cmdmod.cmod_flags & CMOD_KEEPALT) == 0) {
    185        curwin->w_alt_fnum = alt_fnum;
    186      }
    187      empty_fnum = curbuf->b_fnum;
    188    }
    189  }
    190 
    191  restart_edit = 0;               // don't want insert mode in help file
    192 
    193  // Restore KeyTyped, setting 'filetype=help' may reset it.
    194  // It is needed for do_tag top open folds under the cursor.
    195  KeyTyped = old_KeyTyped;
    196 
    197  do_tag(tag, DT_HELP, 1, false, true);
    198 
    199  // Delete the empty buffer if we're not using it.  Careful: autocommands
    200  // may have jumped to another window, check that the buffer is not in a
    201  // window.
    202  if (empty_fnum != 0 && curbuf->b_fnum != empty_fnum) {
    203    buf_T *buf = buflist_findnr(empty_fnum);
    204    if (buf != NULL && buf->b_nwindows == 0) {
    205      wipe_buffer(buf, true);
    206    }
    207  }
    208 
    209  // keep the previous alternate file
    210  if (alt_fnum != 0 && curwin->w_alt_fnum == empty_fnum
    211      && (cmdmod.cmod_flags & CMOD_KEEPALT) == 0) {
    212    curwin->w_alt_fnum = alt_fnum;
    213  }
    214 
    215 erret:
    216  xfree(tag);
    217 }
    218 
    219 /// ":helpclose": Close one help window
    220 void ex_helpclose(exarg_T *eap)
    221 {
    222  FOR_ALL_WINDOWS_IN_TAB(win, curtab) {
    223    if (bt_help(win->w_buffer)) {
    224      win_close(win, false, eap->forceit);
    225      return;
    226    }
    227  }
    228 }
    229 
    230 /// In an argument search for a language specifiers in the form "@xx".
    231 /// Changes the "@" to NUL if found, and returns a pointer to "xx".
    232 ///
    233 /// @return  NULL if not found.
    234 char *check_help_lang(char *arg)
    235 {
    236  int len = (int)strlen(arg);
    237 
    238  if (len >= 3 && arg[len - 3] == '@' && ASCII_ISALPHA(arg[len - 2])
    239      && ASCII_ISALPHA(arg[len - 1])) {
    240    arg[len - 3] = NUL;                 // remove the '@'
    241    return arg + len - 2;
    242  }
    243  return NULL;
    244 }
    245 
    246 /// Return a heuristic indicating how well the given string matches.  The
    247 /// smaller the number, the better the match.  This is the order of priorities,
    248 /// from best match to worst match:
    249 ///      - Match with least alphanumeric characters is better.
    250 ///      - Match with least total characters is better.
    251 ///      - Match towards the start is better.
    252 ///      - Match starting with "+" is worse (feature instead of command)
    253 /// Assumption is made that the matched_string passed has already been found to
    254 /// match some string for which help is requested.  webb.
    255 ///
    256 /// @param offset      offset for match
    257 /// @param wrong_case  no matching case
    258 ///
    259 /// @return  a heuristic indicating how well the given string matches.
    260 int help_heuristic(char *matched_string, int offset, bool wrong_case)
    261  FUNC_ATTR_PURE
    262 {
    263  int num_letters = 0;
    264  for (char *p = matched_string; *p; p++) {
    265    if (ASCII_ISALNUM(*p)) {
    266      num_letters++;
    267    }
    268  }
    269 
    270  // Multiply the number of letters by 100 to give it a much bigger
    271  // weighting than the number of characters.
    272  // If there only is a match while ignoring case, add 5000.
    273  // If the match starts in the middle of a word, add 10000 to put it
    274  // somewhere in the last half.
    275  // If the match is more than 2 chars from the start, multiply by 200 to
    276  // put it after matches at the start.
    277  if (offset > 0
    278      && ASCII_ISALNUM(matched_string[offset])
    279      && ASCII_ISALNUM(matched_string[offset - 1])) {
    280    offset += 10000;
    281  } else if (offset > 2) {
    282    offset *= 200;
    283  }
    284  if (wrong_case) {
    285    offset += 5000;
    286  }
    287  // Features are less interesting than the subjects themselves, but "+"
    288  // alone is not a feature.
    289  if (matched_string[0] == '+' && matched_string[1] != NUL) {
    290    offset += 100;
    291  }
    292  return 100 * num_letters + (int)strlen(matched_string) + offset;
    293 }
    294 
    295 /// Compare functions for qsort() below, that checks the help heuristics number
    296 /// that has been put after the tagname by find_tags().
    297 static int help_compare(const void *s1, const void *s2)
    298 {
    299  char *p1 = *(char **)s1 + strlen(*(char **)s1) + 1;
    300  char *p2 = *(char **)s2 + strlen(*(char **)s2) + 1;
    301 
    302  // Compare by help heuristic number first.
    303  int cmp = strcmp(p1, p2);
    304  if (cmp != 0) {
    305    return cmp;
    306  }
    307 
    308  // Compare by strings as tie-breaker when same heuristic number.
    309  return strcmp(*(char **)s1, *(char **)s2);
    310 }
    311 
    312 /// Find all help tags matching "arg", sort them and return in matches[], with
    313 /// the number of matches in num_matches.
    314 /// The matches will be sorted with a "best" match algorithm.
    315 /// When "keep_lang" is true try keeping the language of the current buffer.
    316 int find_help_tags(const char *arg, int *num_matches, char ***matches, bool keep_lang)
    317 {
    318  Error err = ERROR_INIT;
    319  MAXSIZE_TEMP_ARRAY(args, 1);
    320 
    321  ADD_C(args, CSTR_AS_OBJ(arg));
    322 
    323  Object res = NLUA_EXEC_STATIC("return require'vim._core.help'.escape_subject(...)",
    324                                args, kRetObject, NULL, &err);
    325 
    326  if (ERROR_SET(&err)) {
    327    emsg_multiline(err.msg, "lua_error", HLF_E, true);
    328    api_clear_error(&err);
    329    return FAIL;
    330  }
    331  api_clear_error(&err);
    332 
    333  assert(res.type == kObjectTypeString);
    334  xstrlcpy(IObuff, res.data.string.data, sizeof(IObuff));
    335  api_free_object(res);
    336 
    337  *matches = NULL;
    338  *num_matches = 0;
    339  int flags = TAG_HELP | TAG_REGEXP | TAG_NAMES | TAG_VERBOSE | TAG_NO_TAGFUNC;
    340  if (keep_lang) {
    341    flags |= TAG_KEEP_LANG;
    342  }
    343  if (find_tags(IObuff, num_matches, matches, flags, MAXCOL, NULL) == OK
    344      && *num_matches > 0) {
    345    // Sort the matches found on the heuristic number that is after the
    346    // tag name.
    347    qsort((void *)(*matches), (size_t)(*num_matches),
    348          sizeof(char *), help_compare);
    349    // Delete more than TAG_MANY to reduce the size of the listing.
    350    while (*num_matches > TAG_MANY) {
    351      xfree((*matches)[--*num_matches]);
    352    }
    353  }
    354  return OK;
    355 }
    356 
    357 /// Cleanup matches for help tags:
    358 /// Remove "@ab" if the top of 'helplang' is "ab" and the language of the first
    359 /// tag matches it.  Otherwise remove "@en" if "en" is the only language.
    360 void cleanup_help_tags(int num_file, char **file)
    361 {
    362  char buf[4];
    363  char *p = buf;
    364 
    365  if (p_hlg[0] != NUL && (p_hlg[0] != 'e' || p_hlg[1] != 'n')) {
    366    *p++ = '@';
    367    *p++ = p_hlg[0];
    368    *p++ = p_hlg[1];
    369  }
    370  *p = NUL;
    371 
    372  for (int i = 0; i < num_file; i++) {
    373    int len = (int)strlen(file[i]) - 3;
    374    if (len <= 0) {
    375      continue;
    376    }
    377    if (strcmp(file[i] + len, "@en") == 0) {
    378      // Sorting on priority means the same item in another language may
    379      // be anywhere.  Search all items for a match up to the "@en".
    380      int j;
    381      for (j = 0; j < num_file; j++) {
    382        if (j != i
    383            && (int)strlen(file[j]) == len + 3
    384            && strncmp(file[i], file[j], (size_t)len + 1) == 0) {
    385          break;
    386        }
    387      }
    388      if (j == num_file) {
    389        // item only exists with @en, remove it
    390        file[i][len] = NUL;
    391      }
    392    }
    393  }
    394 
    395  if (*buf != NUL) {
    396    for (int i = 0; i < num_file; i++) {
    397      int len = (int)strlen(file[i]) - 3;
    398      if (len <= 0) {
    399        continue;
    400      }
    401      if (strcmp(file[i] + len, buf) == 0) {
    402        // remove the default language
    403        file[i][len] = NUL;
    404      }
    405    }
    406  }
    407 }
    408 
    409 /// Called when starting to edit a buffer for a help file.
    410 void prepare_help_buffer(void)
    411 {
    412  curbuf->b_help = true;
    413  set_option_direct(kOptBuftype, STATIC_CSTR_AS_OPTVAL("help"), OPT_LOCAL, 0);
    414 
    415  // Always set these options after jumping to a help tag, because the
    416  // user may have an autocommand that gets in the way.
    417  // Accept all ASCII chars for keywords, except ' ', '*', '"', '|', and
    418  // latin1 word characters (for translated help files).
    419  // Only set it when needed, buf_init_chartab() is some work.
    420  char *p = "!-~,^*,^|,^\",192-255";
    421  if (strcmp(curbuf->b_p_isk, p) != 0) {
    422    set_option_direct(kOptIskeyword, CSTR_AS_OPTVAL(p), OPT_LOCAL, 0);
    423    check_buf_options(curbuf);
    424    buf_init_chartab(curbuf, false);
    425  }
    426 
    427  // Don't use the global foldmethod.
    428  set_option_direct(kOptFoldmethod, STATIC_CSTR_AS_OPTVAL("manual"), OPT_LOCAL, 0);
    429 
    430  curbuf->b_p_ts = 8;         // 'tabstop' is 8.
    431  curwin->w_p_list = false;   // No list mode.
    432 
    433  curbuf->b_p_ma = false;     // Not modifiable.
    434  curbuf->b_p_bin = false;    // Reset 'bin' before reading file.
    435  curwin->w_p_nu = 0;         // No line numbers.
    436  curwin->w_p_rnu = 0;        // No relative line numbers.
    437  RESET_BINDING(curwin);      // No scroll or cursor binding.
    438  curwin->w_p_arab = false;   // No arabic mode.
    439  curwin->w_p_rl = false;     // Help window is left-to-right.
    440  curwin->w_p_fen = false;    // No folding in the help window.
    441  curwin->w_p_diff = false;   // No 'diff'.
    442  curwin->w_p_spell = false;  // No spell checking.
    443 
    444  set_buflisted(false);
    445 }
    446 
    447 /// Populate *local-additions* in help.txt
    448 void get_local_additions(void)
    449 {
    450  Error err = ERROR_INIT;
    451  Object res = NLUA_EXEC_STATIC("return require'vim._core.help'.local_additions()",
    452                                (Array)ARRAY_DICT_INIT, kRetNilBool, NULL, &err);
    453  if (ERROR_SET(&err)) {
    454    emsg_multiline(err.msg, "lua_error", HLF_E, true);
    455  }
    456  api_free_object(res);
    457  api_clear_error(&err);
    458 }
    459 
    460 /// ":exusage"
    461 void ex_exusage(exarg_T *eap)
    462 {
    463  do_cmdline_cmd("help ex-cmd-index");
    464 }
    465 
    466 /// ":viusage"
    467 void ex_viusage(exarg_T *eap)
    468 {
    469  do_cmdline_cmd("help normal-index");
    470 }
    471 
    472 /// Generate tags in one help directory
    473 ///
    474 /// @param dir  Path to the doc directory
    475 /// @param ext  Suffix of the help files (".txt", ".itx", ".frx", etc.)
    476 /// @param tagname  Name of the tags file ("tags" for English, "tags-fr" for
    477 ///                 French)
    478 /// @param add_help_tags  Whether to add the "help-tags" tag
    479 /// @param ignore_writeerr  ignore write error
    480 static void helptags_one(char *dir, const char *ext, const char *tagfname, bool add_help_tags,
    481                         bool ignore_writeerr)
    482  FUNC_ATTR_NONNULL_ALL
    483 {
    484  garray_T ga;
    485  int filecount;
    486  char **files;
    487  char *s;
    488 
    489  // Find all *.txt files.
    490  size_t dirlen = xstrlcpy(NameBuff, dir, sizeof(NameBuff));
    491  if (dirlen >= MAXPATHL
    492      || xstrlcat(NameBuff, "/**/*", sizeof(NameBuff)) >= MAXPATHL  // NOLINT
    493      || xstrlcat(NameBuff, ext, sizeof(NameBuff)) >= MAXPATHL) {
    494    emsg(_(e_fnametoolong));
    495    return;
    496  }
    497 
    498  // Note: We cannot just do `&NameBuff` because it is a statically sized array
    499  //       so `NameBuff == &NameBuff` according to C semantics.
    500  char *buff_list[1] = { NameBuff };
    501  const int res = gen_expand_wildcards(1, buff_list, &filecount, &files,
    502                                       EW_FILE|EW_SILENT);
    503  if (res == FAIL || filecount == 0) {
    504    if (!got_int) {
    505      semsg(_("E151: No match: %s"), NameBuff);
    506    }
    507    if (res != FAIL) {
    508      FreeWild(filecount, files);
    509    }
    510    return;
    511  }
    512 
    513  // Open the tags file for writing.
    514  // Do this before scanning through all the files.
    515  memcpy(NameBuff, dir, dirlen + 1);
    516  if (!add_pathsep(NameBuff)
    517      || xstrlcat(NameBuff, tagfname, sizeof(NameBuff)) >= MAXPATHL) {
    518    emsg(_(e_fnametoolong));
    519    return;
    520  }
    521 
    522  FILE *const fd_tags = os_fopen(NameBuff, "w");
    523  if (fd_tags == NULL) {
    524    if (!ignore_writeerr) {
    525      semsg(_("E152: Cannot open %s for writing"), NameBuff);
    526    }
    527    FreeWild(filecount, files);
    528    return;
    529  }
    530 
    531  // If using the "++t" argument or generating tags for "$VIMRUNTIME/doc"
    532  // add the "help-tags" tag.
    533  ga_init(&ga, (int)sizeof(char *), 100);
    534  if (add_help_tags
    535      || path_full_compare("$VIMRUNTIME/doc", dir, false, true) == kEqualFiles) {
    536    size_t s_len = 18 + strlen(tagfname);
    537    s = xmalloc(s_len);
    538    snprintf(s, s_len, "help-tags\t%s\t1\n", tagfname);
    539    GA_APPEND(char *, &ga, s);
    540  }
    541 
    542  // Go over all the files and extract the tags.
    543  for (int fi = 0; fi < filecount && !got_int; fi++) {
    544    FILE *const fd = os_fopen(files[fi], "r");
    545    if (fd == NULL) {
    546      semsg(_("E153: Unable to open %s for reading"), files[fi]);
    547      continue;
    548    }
    549    const char *const fname = files[fi] + dirlen + 1;
    550 
    551    bool in_example = false;
    552    while (!vim_fgets(IObuff, IOSIZE, fd) && !got_int) {
    553      if (in_example) {
    554        // skip over example; a non-white in the first column ends it
    555        if (vim_strchr(" \t\n\r", (uint8_t)IObuff[0])) {
    556          continue;
    557        }
    558        in_example = false;
    559      }
    560      char *p1 = vim_strchr(IObuff, '*');       // find first '*'
    561      while (p1 != NULL) {
    562        char *p2 = strchr(p1 + 1, '*');  // Find second '*'.
    563        if (p2 != NULL && p2 > p1 + 1) {         // Skip "*" and "**".
    564          for (s = p1 + 1; s < p2; s++) {
    565            if (*s == ' ' || *s == '\t' || *s == '|') {
    566              break;
    567            }
    568          }
    569 
    570          // Only accept a *tag* when it consists of valid
    571          // characters, there is white space before it and is
    572          // followed by a white character or end-of-line.
    573          if (s == p2
    574              && (p1 == IObuff || p1[-1] == ' ' || p1[-1] == '\t')
    575              && (vim_strchr(" \t\n\r", (uint8_t)s[1]) != NULL
    576                  || s[1] == NUL)) {
    577            *p2 = NUL;
    578            p1++;
    579            size_t s_len = (size_t)(p2 - p1) + strlen(fname) + 2;
    580            s = xmalloc(s_len);
    581            GA_APPEND(char *, &ga, s);
    582            snprintf(s, s_len, "%s\t%s", p1, fname);
    583 
    584            // find next '*'
    585            p2 = vim_strchr(p2 + 1, '*');
    586          }
    587        }
    588        p1 = p2;
    589      }
    590      size_t off = strlen(IObuff);
    591      if (off >= 2 && IObuff[off - 1] == '\n') {
    592        off -= 2;
    593        while (off > 0 && (ASCII_ISLOWER(IObuff[off]) || ascii_isdigit(IObuff[off]))) {
    594          off--;
    595        }
    596        if (IObuff[off] == '>' && (off == 0 || IObuff[off - 1] == ' ')) {
    597          in_example = true;
    598        }
    599      }
    600      line_breakcheck();
    601    }
    602 
    603    fclose(fd);
    604  }
    605 
    606  FreeWild(filecount, files);
    607 
    608  if (!got_int && ga.ga_data != NULL) {
    609    // Sort the tags.
    610    sort_strings(ga.ga_data, ga.ga_len);
    611 
    612    // Check for duplicates.
    613    for (int i = 1; i < ga.ga_len; i++) {
    614      char *p1 = ((char **)ga.ga_data)[i - 1];
    615      char *p2 = ((char **)ga.ga_data)[i];
    616      while (*p1 == *p2) {
    617        if (*p2 == '\t') {
    618          *p2 = NUL;
    619          vim_snprintf(NameBuff, MAXPATHL,
    620                       _("E154: Duplicate tag \"%s\" in file %s/%s"),
    621                       ((char **)ga.ga_data)[i], dir, p2 + 1);
    622          emsg(NameBuff);
    623          *p2 = '\t';
    624          break;
    625        }
    626        p1++;
    627        p2++;
    628      }
    629    }
    630 
    631    // Write the tags into the file.
    632    for (int i = 0; i < ga.ga_len; i++) {
    633      s = ((char **)ga.ga_data)[i];
    634      if (strncmp(s, "help-tags\t", 10) == 0) {
    635        // help-tags entry was added in formatted form
    636        fputs(s, fd_tags);
    637      } else {
    638        fprintf(fd_tags, "%s\t/" "*", s);
    639        for (char *p1 = s; *p1 != '\t'; p1++) {
    640          // insert backslash before '\\' and '/'
    641          if (*p1 == '\\' || *p1 == '/') {
    642            putc('\\', fd_tags);
    643          }
    644          putc(*p1, fd_tags);
    645        }
    646        fprintf(fd_tags, "*\n");
    647      }
    648    }
    649  }
    650 
    651  GA_DEEP_CLEAR_PTR(&ga);
    652  fclose(fd_tags);          // there is no check for an error...
    653 }
    654 
    655 /// Generate tags in one help directory, taking care of translations.
    656 static void do_helptags(char *dirname, bool add_help_tags, bool ignore_writeerr)
    657  FUNC_ATTR_NONNULL_ALL
    658 {
    659  garray_T ga;
    660  char lang[2];
    661  char ext[5];
    662  char fname[8];
    663  int filecount;
    664  char **files;
    665 
    666  // Get a list of all files in the help directory and in subdirectories.
    667  xstrlcpy(NameBuff, dirname, sizeof(NameBuff));
    668  if (!add_pathsep(NameBuff)
    669      || xstrlcat(NameBuff, "**", sizeof(NameBuff)) >= MAXPATHL) {
    670    emsg(_(e_fnametoolong));
    671    return;
    672  }
    673 
    674  // Note: We cannot just do `&NameBuff` because it is a statically sized array
    675  //       so `NameBuff == &NameBuff` according to C semantics.
    676  char *buff_list[1] = { NameBuff };
    677  if (gen_expand_wildcards(1, buff_list, &filecount, &files,
    678                           EW_FILE|EW_SILENT) == FAIL
    679      || filecount == 0) {
    680    semsg(_("E151: No match: %s"), NameBuff);
    681    return;
    682  }
    683 
    684  // Go over all files in the directory to find out what languages are
    685  // present.
    686  int j;
    687  ga_init(&ga, 1, 10);
    688  for (int i = 0; i < filecount; i++) {
    689    int len = (int)strlen(files[i]);
    690    if (len <= 4) {
    691      continue;
    692    }
    693 
    694    if (STRICMP(files[i] + len - 4, ".txt") == 0) {
    695      // ".txt" -> language "en"
    696      lang[0] = 'e';
    697      lang[1] = 'n';
    698    } else if (files[i][len - 4] == '.'
    699               && ASCII_ISALPHA(files[i][len - 3])
    700               && ASCII_ISALPHA(files[i][len - 2])
    701               && TOLOWER_ASC(files[i][len - 1]) == 'x') {
    702      // ".abx" -> language "ab"
    703      lang[0] = (char)TOLOWER_ASC(files[i][len - 3]);
    704      lang[1] = (char)TOLOWER_ASC(files[i][len - 2]);
    705    } else {
    706      continue;
    707    }
    708 
    709    // Did we find this language already?
    710    for (j = 0; j < ga.ga_len; j += 2) {
    711      if (strncmp(lang, ((char *)ga.ga_data) + j, 2) == 0) {
    712        break;
    713      }
    714    }
    715    if (j == ga.ga_len) {
    716      // New language, add it.
    717      ga_grow(&ga, 2);
    718      ((char *)ga.ga_data)[ga.ga_len++] = lang[0];
    719      ((char *)ga.ga_data)[ga.ga_len++] = lang[1];
    720    }
    721  }
    722 
    723  // Loop over the found languages to generate a tags file for each one.
    724  for (j = 0; j < ga.ga_len; j += 2) {
    725    STRCPY(fname, "tags-xx");
    726    fname[5] = ((char *)ga.ga_data)[j];
    727    fname[6] = ((char *)ga.ga_data)[j + 1];
    728    if (fname[5] == 'e' && fname[6] == 'n') {
    729      // English is an exception: use ".txt" and "tags".
    730      fname[4] = NUL;
    731      STRCPY(ext, ".txt");
    732    } else {
    733      // Language "ab" uses ".abx" and "tags-ab".
    734      STRCPY(ext, ".xxx");
    735      ext[1] = fname[5];
    736      ext[2] = fname[6];
    737    }
    738    helptags_one(dirname, ext, fname, add_help_tags, ignore_writeerr);
    739  }
    740 
    741  ga_clear(&ga);
    742  FreeWild(filecount, files);
    743 }
    744 
    745 static bool helptags_cb(int num_fnames, char **fnames, bool all, void *cookie)
    746  FUNC_ATTR_NONNULL_ALL
    747 {
    748  for (int i = 0; i < num_fnames; i++) {
    749    do_helptags(fnames[i], *(bool *)cookie, true);
    750    if (!all) {
    751      return true;
    752    }
    753  }
    754 
    755  return num_fnames > 0;
    756 }
    757 
    758 /// ":helptags"
    759 void ex_helptags(exarg_T *eap)
    760 {
    761  expand_T xpc;
    762  bool add_help_tags = false;
    763 
    764  // Check for ":helptags ++t {dir}".
    765  if (strncmp(eap->arg, "++t", 3) == 0 && ascii_iswhite(eap->arg[3])) {
    766    add_help_tags = true;
    767    eap->arg = skipwhite(eap->arg + 3);
    768  }
    769 
    770  if (strcmp(eap->arg, "ALL") == 0) {
    771    do_in_path(p_rtp, "", "doc", DIP_ALL + DIP_DIR, helptags_cb, &add_help_tags);
    772  } else {
    773    ExpandInit(&xpc);
    774    xpc.xp_context = EXPAND_DIRECTORIES;
    775    char *dirname =
    776      ExpandOne(&xpc, eap->arg, NULL, WILD_LIST_NOTFOUND|WILD_SILENT, WILD_EXPAND_FREE);
    777    if (dirname == NULL || !os_isdir(dirname)) {
    778      semsg(_("E150: Not a directory: %s"), eap->arg);
    779    } else {
    780      do_helptags(dirname, add_help_tags, false);
    781    }
    782    xfree(dirname);
    783  }
    784 }