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 }