| /* 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 (ERROR_IF_ANY_POPUP_WINDOW) |
| return; |
| |
| 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(_(e_dont_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(_(e_sorry_no_str_help_for_str), lang, arg); |
| else |
| #endif |
| semsg(_(e_sorry_no_help_for_str), 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.cmod_tab != 0) |
| { |
| if (cmdmod.cmod_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.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; |
| (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.cmod_flags & CMOD_KEEPALT) == 0) |
| 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.cmod_flags & CMOD_KEEPALT) == 0) |
| 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; |
| // 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"}, |
| {"/\\*", "/\\\\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 char *(expr_table[]) = {"!=?", "!~?", "<=?", "<?", "==?", "=~?", |
| ">=?", ">?", "is?", "isnot?"}; |
| int flags; |
| |
| 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 (i = (int)ARRAY_LENGTH(expr_table); --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 |
| // '*'are changed to "star", otherwise '*' is recognized as a wildcard. |
| for (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((char_u *)"%_z@", arg[1]) != NULL |
| && arg[2] != NUL))) |
| { |
| vim_snprintf((char *)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 (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 < ' ') |
| { |
| *d++ = *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++ = '\\'; |
| |
| // 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. |
| // When adding an option here, also update the help file helphelp.txt. |
| |
| // 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 = (char_u *)"!-~,^*,^|,^\",192-255"; |
| 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_give_err((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 = ml_get_buf_len(curbuf, lnum); |
| 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) |
| { |
| f1 = fnames[i1]; |
| t1 = gettail(f1); |
| e1 = vim_strrchr(t1, '.'); |
| if (fnamecmp(e1, ".txt") != 0 |
| && fnamecmp(e1, fname + 4) != 0) |
| { |
| // Not .txt and not .abx, remove it. |
| VIM_CLEAR(fnames[i1]); |
| continue; |
| } |
| |
| for (i2 = i1 + 1; i2 < fcount; ++i2) |
| { |
| f2 = fnames[i2]; |
| if (f2 == NULL) |
| continue; |
| t2 = gettail(f2); |
| e2 = vim_strrchr(t2, '.'); |
| 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 res; |
| 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 in_example; |
| int len; |
| int mix = FALSE; // detected mixed encodings |
| |
| // Find all *.txt files. |
| dirlen = (int)STRLEN(dir); |
| STRCPY(NameBuff, dir); |
| STRCAT(NameBuff, "/**/*"); |
| STRCAT(NameBuff, ext); |
| res = gen_expand_wildcards(1, &NameBuff, &filecount, &files, |
| EW_FILE|EW_SILENT); |
| if (res == FAIL || filecount == 0) |
| { |
| if (!got_int) |
| semsg(_(e_no_match_str_1), NameBuff); |
| if (res != FAIL) |
| FreeWild(filecount, files); |
| 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(_(e_cannot_open_str_for_writing_1), NameBuff); |
| FreeWild(filecount, files); |
| return; |
| } |
| |
| // If using the "++t" argument or generating tags for "$VIMRUNTIME/doc" |
| // add the "help-tags" tag. |
| ga_init2(&ga, 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(_(e_unable_to_open_str_for_reading), files[fi]); |
| continue; |
| } |
| fname = files[fi] + dirlen + 1; |
| |
| in_example = FALSE; |
| 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(_(e_mix_of_help_file_encodings_within_language_str), 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((char_u *)" \t\n\r", IObuff[0])) |
| continue; |
| in_example = 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; |
| } |
| len = (int)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) |
| { |
| // 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, |
| _(e_duplicate_tag_str_in_file_str_str), |
| ((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(_(e_no_match_str_1), 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) |
| 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] = 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(_(e_not_a_directory_str), eap->arg); |
| else |
| do_helptags(dirname, add_help_tags, FALSE); |
| vim_free(dirname); |
| } |
| } |