Files
neovim/src/nvim/help.c
Daniil Zhukov 9d48266bed fix(coverity/471380): null dereference in get_local_additions()
strrchr returns null pointer if '.' is not present in file name. Notice
that filenames are filtered to match "doc/*.??[tx]" pattern earlier so
we shouldn't expect null pointer here. However later in code strrchr
return value is checked so it seems better and more consistent to do the
same here too.
2024-01-27 23:00:24 +04:00

1171 lines
36 KiB
C

// help.c: functions for Vim help
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "nvim/ascii_defs.h"
#include "nvim/buffer.h"
#include "nvim/buffer_defs.h"
#include "nvim/change.h"
#include "nvim/charset.h"
#include "nvim/cmdexpand.h"
#include "nvim/cmdexpand_defs.h"
#include "nvim/ex_cmds.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/ex_docmd.h"
#include "nvim/extmark_defs.h"
#include "nvim/fileio.h"
#include "nvim/garray.h"
#include "nvim/garray_defs.h"
#include "nvim/gettext_defs.h"
#include "nvim/globals.h"
#include "nvim/help.h"
#include "nvim/macros_defs.h"
#include "nvim/mark.h"
#include "nvim/mbyte.h"
#include "nvim/mbyte_defs.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
#include "nvim/message.h"
#include "nvim/option.h"
#include "nvim/option_defs.h"
#include "nvim/option_vars.h"
#include "nvim/optionstr.h"
#include "nvim/os/fs.h"
#include "nvim/os/input.h"
#include "nvim/os/os.h"
#include "nvim/os/os_defs.h"
#include "nvim/path.h"
#include "nvim/pos_defs.h"
#include "nvim/runtime.h"
#include "nvim/strings.h"
#include "nvim/tag.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"
#include "nvim/window.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "help.c.generated.h"
#endif
/// ":help": open a read-only window on a help file
void ex_help(exarg_T *eap)
{
char *arg;
FILE *helpfd; // file descriptor of help file
win_T *wp;
int num_matches;
char **matches;
int empty_fnum = 0;
int alt_fnum = 0;
const bool old_KeyTyped = KeyTyped;
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 = "";
}
// remove trailing blanks
char *p = arg + strlen(arg) - 1;
while (p > arg && ascii_iswhite(*p) && p[-1] != '\\') {
*p-- = NUL;
}
// Check for a specified language
char *lang = check_help_lang(arg);
// When no argument given go to the index.
if (*arg == NUL) {
arg = "help.txt";
}
// Check if there is a match for the argument.
int n = find_help_tags(arg, &num_matches, &matches, eap != NULL && eap->forceit);
int i = 0;
if (n != FAIL && lang != NULL) {
// Find first item with the requested language.
for (i = 0; i < num_matches; i++) {
int len = (int)strlen(matches[i]);
if (len > 3 && matches[i][len - 3] == '@'
&& STRICMP(matches[i] + len - 2, lang) == 0) {
break;
}
}
}
if (i >= num_matches || n == FAIL) {
if (lang != NULL) {
semsg(_("E661: Sorry, no '%s' help for %s"), lang, arg);
} else {
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.
char *tag = xstrdup(matches[i]);
FreeWild(num_matches, matches);
// Re-use an existing help window or open a new one.
// Always open a new one for ":tab help".
if (!bt_help(curwin->w_buffer) || cmdmod.cmod_tab != 0) {
if (cmdmod.cmod_tab != 0) {
wp = NULL;
} else {
wp = NULL;
FOR_ALL_WINDOWS_IN_TAB(wp2, curtab) {
if (bt_help(wp2->w_buffer)) {
wp = wp2;
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 = os_fopen(p_hf, READBIN)) == NULL) {
smsg(0, _("Sorry, help file \"%s\" not found"), p_hf);
goto erret;
}
fclose(helpfd);
// Split off help window; put it at far top if no position
// specified, the current window is vertically split and
// narrow.
n = WSP_HELP;
if (cmdmod.cmod_split == 0 && curwin->w_width != Columns
&& curwin->w_width < 80) {
n |= p_sb ? WSP_BOT : WSP_TOP;
}
if (win_split(0, n) == FAIL) {
goto erret;
}
if (curwin->w_height < p_hh) {
win_setheight((int)p_hh);
}
// Open help file (do_ecmd() will set b_help flag, readfile() will
// set b_p_ro flag).
// Set the alternate file to the previously edited file.
alt_fnum = curbuf->b_fnum;
do_ecmd(0, NULL, NULL, NULL, ECMD_LASTL,
ECMD_HIDE + ECMD_SET_HELP,
NULL); // buffer is still open, don't store info
if ((cmdmod.cmod_flags & CMOD_KEEPALT) == 0) {
curwin->w_alt_fnum = alt_fnum;
}
empty_fnum = curbuf->b_fnum;
}
}
restart_edit = 0; // don't want insert mode in help file
// Restore KeyTyped, setting 'filetype=help' may reset it.
// It is needed for do_tag top open folds under the cursor.
KeyTyped = old_KeyTyped;
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_T *buf = buflist_findnr(empty_fnum);
if (buf != NULL && buf->b_nwindows == 0) {
wipe_buffer(buf, true);
}
}
// keep the previous alternate file
if (alt_fnum != 0 && curwin->w_alt_fnum == empty_fnum
&& (cmdmod.cmod_flags & CMOD_KEEPALT) == 0) {
curwin->w_alt_fnum = alt_fnum;
}
erret:
xfree(tag);
}
/// ":helpclose": Close one help window
void ex_helpclose(exarg_T *eap)
{
FOR_ALL_WINDOWS_IN_TAB(win, curtab) {
if (bt_help(win->w_buffer)) {
win_close(win, false, eap->forceit);
return;
}
}
}
/// In an argument search for a language specifiers in the form "@xx".
/// Changes the "@" to NUL if found, and returns a pointer to "xx".
///
/// @return NULL if not found.
char *check_help_lang(char *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;
}
/// 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.
///
/// @param offset offset for match
/// @param wrong_case no matching case
///
/// @return a heuristic indicating how well the given string matches.
int help_heuristic(char *matched_string, int offset, bool wrong_case)
FUNC_ATTR_PURE
{
int num_letters = 0;
for (char *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 (offset > 0
&& ASCII_ISALNUM(matched_string[offset])
&& 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 100 * num_letters + (int)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 **)s1 + strlen(*(char **)s1) + 1;
char *p2 = *(char **)s2 + strlen(*(char **)s2) + 1;
// Compare by help heuristic number first.
int 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(const char *arg, int *num_matches, char ***matches, bool keep_lang)
{
// Specific tags that either have a specific replacement or won't go
// through the generic rules.
static char *(except_tbl[][2]) = {
{ "*", "star" },
{ "g*", "gstar" },
{ "[*", "[star" },
{ "]*", "]star" },
{ ":*", ":star" },
{ "/*", "/star" }, // NOLINT
{ "/\\*", "/\\\\star" },
{ "\"*", "quotestar" },
{ "**", "starstar" },
{ "cpo-*", "cpo-star" },
{ "/\\(\\)", "/\\\\(\\\\)" },
{ "/\\%(\\)", "/\\\\%(\\\\)" },
{ "?", "?" },
{ "??", "??" },
{ ":?", ":?" },
{ "?<CR>", "?<CR>" },
{ "g?", "g?" },
{ "g?g?", "g?g?" },
{ "g??", "g??" },
{ "-?", "-?" },
{ "q?", "q?" },
{ "v_g?", "v_g?" },
{ "/\\?", "/\\\\?" },
{ "/\\z(\\)", "/\\\\z(\\\\)" },
{ "\\=", "\\\\=" },
{ ":s\\=", ":s\\\\=" },
{ "[count]", "\\[count]" },
{ "[quotex]", "\\[quotex]" },
{ "[range]", "\\[range]" },
{ ":[range]", ":\\[range]" },
{ "[pattern]", "\\[pattern]" },
{ "\\|", "\\\\bar" },
{ "\\%$", "/\\\\%\\$" },
{ "s/\\~", "s/\\\\\\~" },
{ "s/\\U", "s/\\\\U" },
{ "s/\\L", "s/\\\\L" },
{ "s/\\1", "s/\\\\1" },
{ "s/\\2", "s/\\\\2" },
{ "s/\\3", "s/\\\\3" },
{ "s/\\9", "s/\\\\9" },
{ NULL, NULL }
};
static const char *(expr_table[]) = {
"!=?", "!~?", "<=?", "<?", "==?", "=~?",
">=?", ">?", "is?", "isnot?"
};
char *d = IObuff; // assume IObuff is long enough!
d[0] = NUL;
if (STRNICMP(arg, "expr-", 5) == 0) {
// When the string starting with "expr-" and containing '?' and matches
// the table, it is taken literally (but ~ is escaped). Otherwise '?'
// is recognized as a wildcard.
for (int i = (int)ARRAY_SIZE(expr_table); --i >= 0;) {
if (strcmp(arg + 5, expr_table[i]) == 0) {
for (int si = 0, di = 0;; si++) {
if (arg[si] == '~') {
d[di++] = '\\';
}
d[di++] = arg[si];
if (arg[si] == NUL) {
break;
}
}
break;
}
}
} else {
// Recognize a few exceptions to the rule. Some strings that contain
// '*'are changed to "star", otherwise '*' is recognized as a wildcard.
for (int i = 0; except_tbl[i][0] != NULL; i++) {
if (strcmp(arg, except_tbl[i][0]) == 0) {
STRCPY(d, except_tbl[i][1]);
break;
}
}
}
if (d[0] == NUL) { // no match in table
// Replace "\S" with "/\\S", etc. Otherwise every tag is matched.
// Also replace "\%^" and "\%(", they match every tag too.
// Also "\zs", "\z1", etc.
// Also "\@<", "\@=", "\@<=", etc.
// And also "\_$" and "\_^".
if (arg[0] == '\\'
&& ((arg[1] != NUL && arg[2] == NUL)
|| (vim_strchr("%_z@", (uint8_t)arg[1]) != NULL
&& arg[2] != NUL))) {
vim_snprintf(d, IOSIZE, "/\\\\%s", arg + 1);
// Check for "/\\_$", should be "/\\_\$"
if (d[3] == '_' && d[4] == '$') {
STRCPY(d + 4, "\\$");
}
} else {
// Replace:
// "[:...:]" with "\[:...:]"
// "[++...]" with "\[++...]"
// "\{" with "\\{" -- matching "} \}"
if ((arg[0] == '[' && (arg[1] == ':'
|| (arg[1] == '+' && arg[2] == '+')))
|| (arg[0] == '\\' && arg[1] == '{')) {
*d++ = '\\';
}
// If tag starts with "('", skip the "(". Fixes CTRL-] on ('option'.
if (*arg == '(' && arg[1] == '\'') {
arg++;
}
for (const char *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 ((uint8_t)(*s) < ' '
|| (*s == '^' && s[1]
&& (ASCII_ISALPHA(s[1]) || vim_strchr("?@[\\]^", (uint8_t)s[1]) != NULL))) {
if (d > IObuff && d[-1] != '_' && d[-1] != '\\') {
*d++ = '_'; // prepend a '_' to make x_CTRL-x
}
STRCPY(d, "CTRL-");
d += 5;
if (*s < ' ') {
*d++ = (char)(*s + '@');
if (d[-1] == '\\') {
*d++ = '\\'; // double a backslash
}
} else {
*d++ = *++s;
}
if (s[1] != NUL && s[1] != '_') {
*d++ = '_'; // append a '_'
}
continue;
} else if (*s == '^') { // "^" or "CTRL-^" or "^_"
*d++ = '\\';
} else if (s[0] == '\\' && s[1] != '\\' && *arg == '/' && s == arg + 1) {
// Insert a backslash before a backslash after a slash, for search
// pattern tags: "/\|" --> "/\\|".
*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 '}'. Fixes CTRL-] on '{address}'.
if (*s == '}' && s > arg && *arg == '{') {
break;
}
}
*d = NUL;
if (*IObuff == '`') {
if (d > IObuff + 2 && d[-1] == '`') {
// remove the backticks from `command`
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`,
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`\.
memmove(IObuff, IObuff + 1, strlen(IObuff));
d[-4] = NUL;
}
}
}
}
*matches = NULL;
*num_matches = 0;
int 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, 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 *), help_compare);
// Delete more than TAG_MANY to reduce the size of the listing.
while (*num_matches > TAG_MANY) {
xfree((*matches)[--*num_matches]);
}
}
return OK;
}
/// 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 **file)
{
char buf[4];
char *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 (int i = 0; i < num_file; i++) {
int 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".
int j;
for (j = 0; j < num_file; j++) {
if (j != i
&& (int)strlen(file[j]) == len + 3
&& strncmp(file[i], file[j], (size_t)len + 1) == 0) {
break;
}
}
if (j == num_file) {
// item only exists with @en, remove it
file[i][len] = NUL;
}
}
}
if (*buf != NUL) {
for (int i = 0; i < num_file; i++) {
int 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;
}
}
}
}
/// Called when starting to edit a buffer for a help file.
void prepare_help_buffer(void)
{
curbuf->b_help = true;
set_string_option_direct(kOptBuftype, "help", OPT_LOCAL, 0);
// 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.
char *p = "!-~,^*,^|,^\",192-255";
if (strcmp(curbuf->b_p_isk, p) != 0) {
set_string_option_direct(kOptIskeyword, p, OPT_LOCAL, 0);
check_buf_options(curbuf);
buf_init_chartab(curbuf, false);
}
// Don't use the global foldmethod.
set_string_option_direct(kOptFoldmethod, "manual", OPT_LOCAL, 0);
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.
curwin->w_p_arab = false; // No arabic mode.
curwin->w_p_rl = false; // Help window is left-to-right.
curwin->w_p_fen = false; // No folding in the help window.
curwin->w_p_diff = false; // No 'diff'.
curwin->w_p_spell = false; // No spell checking.
set_buflisted(false);
}
/// After reading a help file: if help.txt, populate *local-additions*
void get_local_additions(void)
{
// In the "help.txt" and "help.abx" file, add the locally added help
// files. This uses the very first line in the help file.
char *const fname = path_tail(curbuf->b_fname);
if (path_fnamecmp(fname, "help.txt") == 0
|| (path_fnamencmp(fname, "help.", 5) == 0
&& ASCII_ISALPHA(fname[5])
&& ASCII_ISALPHA(fname[6])
&& TOLOWER_ASC(fname[7]) == 'x'
&& fname[8] == NUL)) {
for (linenr_T lnum = 1; lnum < curbuf->b_ml.ml_line_count; lnum++) {
char *line = ml_get_buf(curbuf, lnum);
if (strstr(line, "*local-additions*") == NULL) {
continue;
}
int lnum_start = lnum;
// Go through all directories in 'runtimepath', skipping
// $VIMRUNTIME.
char *p = p_rtp;
while (*p != NUL) {
copy_option_part(&p, NameBuff, MAXPATHL, ",");
char *const rt = vim_getenv("VIMRUNTIME");
if (rt != NULL
&& path_full_compare(rt, NameBuff, false, true) != kEqualFiles) {
int fcount;
char **fnames;
vimconv_T vc;
// Find all "doc/ *.txt" files in this directory.
if (!add_pathsep(NameBuff)
|| xstrlcat(NameBuff, "doc/*.??[tx]", // NOLINT
sizeof(NameBuff)) >= MAXPATHL) {
emsg(_(e_fnametoolong));
continue;
}
// Note: We cannot just do `&NameBuff` because it is a statically sized array
// so `NameBuff == &NameBuff` according to C semantics.
char *buff_list[1] = { NameBuff };
if (gen_expand_wildcards(1, buff_list, &fcount,
&fnames, EW_FILE|EW_SILENT) == OK
&& fcount > 0) {
char *s;
char *cp;
// If foo.abx is found use it instead of foo.txt in
// the same directory.
for (int i1 = 0; i1 < fcount; i1++) {
const char *const f1 = fnames[i1];
const char *const t1 = path_tail(f1);
const char *const e1 = strrchr(t1, '.');
if (e1 == NULL) {
continue;
}
if (path_fnamecmp(e1, ".txt") != 0
&& path_fnamecmp(e1, fname + 4) != 0) {
// Not .txt and not .abx, remove it.
XFREE_CLEAR(fnames[i1]);
continue;
}
for (int i2 = i1 + 1; i2 < fcount; i2++) {
const char *const f2 = fnames[i2];
if (f2 == NULL) {
continue;
}
const char *const t2 = path_tail(f2);
const char *const e2 = strrchr(t2, '.');
if (e2 == NULL) {
continue;
}
if (e1 - f1 != e2 - f2
|| path_fnamencmp(f1, f2, (size_t)(e1 - f1)) != 0) {
continue;
}
if (path_fnamecmp(e1, ".txt") == 0
&& path_fnamecmp(e2, fname + 4) == 0) {
// use .abx instead of .txt
XFREE_CLEAR(fnames[i1]);
}
}
}
for (int fi = 0; fi < fcount; fi++) {
if (fnames[fi] == NULL) {
continue;
}
FILE *const fd = os_fopen(fnames[fi], "r");
if (fd == NULL) {
continue;
}
vim_fgets(IObuff, IOSIZE, fd);
if (IObuff[0] == '*'
&& (s = vim_strchr(IObuff + 1, '*'))
!= NULL) {
TriState this_utf = kNone;
// 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 ((uint8_t)(*s) >= 0x80 && this_utf != kFalse) {
this_utf = kTrue;
const int l = utf_ptr2len(s);
if (l == 1) {
this_utf = kFalse;
}
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,
(this_utf == kTrue ? "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, 0, false);
if (cp != IObuff) {
xfree(cp);
}
lnum++;
}
fclose(fd);
}
FreeWild(fcount, fnames);
}
}
xfree(rt);
}
linenr_T appended = lnum - lnum_start;
if (appended) {
mark_adjust(lnum_start + 1, (linenr_T)MAXLNUM, appended, 0, kExtmarkUndo);
buf_redraw_changed_lines_later(curbuf, lnum_start + 1, lnum_start + 1, appended);
}
break;
}
}
}
/// ":exusage"
void ex_exusage(exarg_T *eap)
{
do_cmdline_cmd("help ex-cmd-index");
}
/// ":viusage"
void ex_viusage(exarg_T *eap)
{
do_cmdline_cmd("help normal-index");
}
/// Generate tags in one help directory
///
/// @param dir Path to the doc directory
/// @param ext Suffix of the help files (".txt", ".itx", ".frx", etc.)
/// @param tagname Name of the tags file ("tags" for English, "tags-fr" for
/// French)
/// @param add_help_tags Whether to add the "help-tags" tag
/// @param ignore_writeerr ignore write error
static void helptags_one(char *dir, const char *ext, const char *tagfname, bool add_help_tags,
bool ignore_writeerr)
FUNC_ATTR_NONNULL_ALL
{
garray_T ga;
int filecount;
char **files;
char *s;
TriState utf8 = kNone;
bool mix = false; // detected mixed encodings
// Find all *.txt files.
size_t dirlen = xstrlcpy(NameBuff, dir, sizeof(NameBuff));
if (dirlen >= MAXPATHL
|| xstrlcat(NameBuff, "/**/*", sizeof(NameBuff)) >= MAXPATHL // NOLINT
|| xstrlcat(NameBuff, ext, sizeof(NameBuff)) >= MAXPATHL) {
emsg(_(e_fnametoolong));
return;
}
// Note: We cannot just do `&NameBuff` because it is a statically sized array
// so `NameBuff == &NameBuff` according to C semantics.
char *buff_list[1] = { NameBuff };
const int res = gen_expand_wildcards(1, buff_list, &filecount, &files,
EW_FILE|EW_SILENT);
if (res == FAIL || filecount == 0) {
if (!got_int) {
semsg(_("E151: No match: %s"), NameBuff);
}
if (res != FAIL) {
FreeWild(filecount, files);
}
return;
}
// Open the tags file for writing.
// Do this before scanning through all the files.
memcpy(NameBuff, dir, dirlen + 1);
if (!add_pathsep(NameBuff)
|| xstrlcat(NameBuff, tagfname, sizeof(NameBuff)) >= MAXPATHL) {
emsg(_(e_fnametoolong));
return;
}
FILE *const fd_tags = os_fopen(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_init(&ga, (int)sizeof(char *), 100);
if (add_help_tags
|| path_full_compare("$VIMRUNTIME/doc", dir, false, true) == kEqualFiles) {
size_t s_len = 18 + strlen(tagfname);
s = xmalloc(s_len);
snprintf(s, s_len, "help-tags\t%s\t1\n", tagfname);
GA_APPEND(char *, &ga, s);
}
// Go over all the files and extract the tags.
for (int fi = 0; fi < filecount && !got_int; fi++) {
FILE *const fd = os_fopen(files[fi], "r");
if (fd == NULL) {
semsg(_("E153: Unable to open %s for reading"), files[fi]);
continue;
}
const char *const fname = files[fi] + dirlen + 1;
bool in_example = false;
bool 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.
TriState this_utf8 = kNone;
for (s = IObuff; *s != NUL; s++) {
if ((uint8_t)(*s) >= 0x80) {
this_utf8 = kTrue;
const int l = utf_ptr2len(s);
if (l == 1) {
// Illegal UTF-8 byte sequence.
this_utf8 = kFalse;
break;
}
s += l - 1;
}
}
if (this_utf8 == kNone) { // only ASCII characters found
this_utf8 = kFalse;
}
if (utf8 == kNone) { // 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;
}
if (in_example) {
// skip over example; a non-white in the first column ends it
if (vim_strchr(" \t\n\r", (uint8_t)IObuff[0])) {
continue;
}
in_example = false;
}
char *p1 = vim_strchr(IObuff, '*'); // find first '*'
while (p1 != NULL) {
char *p2 = strchr(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(" \t\n\r", (uint8_t)s[1]) != NULL
|| s[1] == '\0')) {
*p2 = '\0';
p1++;
size_t s_len = (size_t)(p2 - p1) + strlen(fname) + 2;
s = xmalloc(s_len);
GA_APPEND(char *, &ga, s);
snprintf(s, s_len, "%s\t%s", p1, fname);
// find next '*'
p2 = vim_strchr(p2 + 1, '*');
}
}
p1 = p2;
}
size_t len = strlen(IObuff);
if ((len == 2 && strcmp(&IObuff[len - 2], ">\n") == 0)
|| (len >= 3 && strcmp(&IObuff[len - 3], " >\n") == 0)) {
in_example = true;
}
line_breakcheck();
}
fclose(fd);
}
FreeWild(filecount, files);
if (!got_int && ga.ga_data != NULL) {
// Sort the tags.
sort_strings(ga.ga_data, ga.ga_len);
// Check for duplicates.
for (int i = 1; i < ga.ga_len; i++) {
char *p1 = ((char **)ga.ga_data)[i - 1];
char *p2 = ((char **)ga.ga_data)[i];
while (*p1 == *p2) {
if (*p2 == '\t') {
*p2 = NUL;
vim_snprintf(NameBuff, MAXPATHL,
_("E154: Duplicate tag \"%s\" in file %s/%s"),
((char **)ga.ga_data)[i], dir, p2 + 1);
emsg(NameBuff);
*p2 = '\t';
break;
}
p1++;
p2++;
}
}
if (utf8 == kTrue) {
fprintf(fd_tags, "!_TAG_FILE_ENCODING\tutf-8\t//\n");
}
// Write the tags into the file.
for (int i = 0; i < ga.ga_len; i++) {
s = ((char **)ga.ga_data)[i];
if (strncmp(s, "help-tags\t", 10) == 0) {
// help-tags entry was added in formatted form
fputs(s, fd_tags);
} else {
fprintf(fd_tags, "%s\t/" "*", s);
for (char *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
}
GA_DEEP_CLEAR_PTR(&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 *dirname, bool add_help_tags, bool ignore_writeerr)
FUNC_ATTR_NONNULL_ALL
{
garray_T ga;
char lang[2];
char ext[5];
char fname[8];
int filecount;
char **files;
// Get a list of all files in the help directory and in subdirectories.
xstrlcpy(NameBuff, dirname, sizeof(NameBuff));
if (!add_pathsep(NameBuff)
|| xstrlcat(NameBuff, "**", sizeof(NameBuff)) >= MAXPATHL) {
emsg(_(e_fnametoolong));
return;
}
// Note: We cannot just do `&NameBuff` because it is a statically sized array
// so `NameBuff == &NameBuff` according to C semantics.
char *buff_list[1] = { NameBuff };
if (gen_expand_wildcards(1, buff_list, &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.
int j;
ga_init(&ga, 1, 10);
for (int i = 0; i < filecount; i++) {
int len = (int)strlen(files[i]);
if (len <= 4) {
continue;
}
if (STRICMP(files[i] + len - 4, ".txt") == 0) {
// ".txt" -> language "en"
lang[0] = 'e';
lang[1] = 'n';
} else if (files[i][len - 4] == '.'
&& ASCII_ISALPHA(files[i][len - 3])
&& ASCII_ISALPHA(files[i][len - 2])
&& TOLOWER_ASC(files[i][len - 1]) == 'x') {
// ".abx" -> language "ab"
lang[0] = (char)TOLOWER_ASC(files[i][len - 3]);
lang[1] = (char)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 *)ga.ga_data) + j, 2) == 0) {
break;
}
}
if (j == ga.ga_len) {
// New language, add it.
ga_grow(&ga, 2);
((char *)ga.ga_data)[ga.ga_len++] = lang[0];
((char *)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 *)ga.ga_data)[j];
fname[6] = ((char *)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);
}
static bool helptags_cb(int num_fnames, char **fnames, bool all, void *cookie)
FUNC_ATTR_NONNULL_ALL
{
for (int i = 0; i < num_fnames; i++) {
do_helptags(fnames[i], *(bool *)cookie, true);
if (!all) {
return true;
}
}
return num_fnames > 0;
}
/// ":helptags"
void ex_helptags(exarg_T *eap)
{
expand_T xpc;
bool add_help_tags = false;
// Check for ":helptags ++t {dir}".
if (strncmp(eap->arg, "++t", 3) == 0 && ascii_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, "", "doc", DIP_ALL + DIP_DIR, helptags_cb, &add_help_tags);
} else {
ExpandInit(&xpc);
xpc.xp_context = EXPAND_DIRECTORIES;
char *dirname =
ExpandOne(&xpc, eap->arg, NULL, WILD_LIST_NOTFOUND|WILD_SILENT, WILD_EXPAND_FREE);
if (dirname == NULL || !os_isdir(dirname)) {
semsg(_("E150: Not a directory: %s"), eap->arg);
} else {
do_helptags(dirname, add_help_tags, false);
}
xfree(dirname);
}
}