diff options
| author | Josh Rahm <rahm@google.com> | 2022-08-19 12:26:08 -0600 |
|---|---|---|
| committer | Josh Rahm <rahm@google.com> | 2022-08-19 13:06:41 -0600 |
| commit | a7237662f96933efe29eed8212464571e3778cd0 (patch) | |
| tree | 27930202726b4251437c8cfa53069f65b4db90dc /src/nvim/spellsuggest.c | |
| parent | 02292344929069ea63c0bb872cc22d552d86b67f (diff) | |
| parent | b2f979b30beac67906b2dd717fcb6a34f46f5e54 (diff) | |
| download | rneovim-tmp.tar.gz rneovim-tmp.tar.bz2 rneovim-tmp.zip | |
Merge branch 'master' of https://github.com/neovim/neovim into rahmtmp
Diffstat (limited to 'src/nvim/spellsuggest.c')
| -rw-r--r-- | src/nvim/spellsuggest.c | 3800 |
1 files changed, 3800 insertions, 0 deletions
diff --git a/src/nvim/spellsuggest.c b/src/nvim/spellsuggest.c new file mode 100644 index 0000000000..b4a9bed437 --- /dev/null +++ b/src/nvim/spellsuggest.c @@ -0,0 +1,3800 @@ +// This is an open source non-commercial project. Dear PVS-Studio, please check +// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + +// spellsuggest.c: functions for spelling suggestions + +#include "nvim/ascii.h" +#include "nvim/change.h" +#include "nvim/charset.h" +#include "nvim/cursor.h" +#include "nvim/eval.h" +#include "nvim/fileio.h" +#include "nvim/garray.h" +#include "nvim/getchar.h" +#include "nvim/hashtab.h" +#include "nvim/input.h" +#include "nvim/mbyte.h" +#include "nvim/memline.h" +#include "nvim/memory.h" +#include "nvim/message.h" +#include "nvim/option.h" +#include "nvim/os/fs.h" +#include "nvim/os/input.h" +#include "nvim/profile.h" +#include "nvim/screen.h" +#include "nvim/spell.h" +#include "nvim/spell_defs.h" +#include "nvim/spellfile.h" +#include "nvim/spellsuggest.h" +#include "nvim/strings.h" +#include "nvim/ui.h" +#include "nvim/undo.h" +#include "nvim/vim.h" + +// Use this to adjust the score after finding suggestions, based on the +// suggested word sounding like the bad word. This is much faster than doing +// it for every possible suggestion. +// Disadvantage: When "the" is typed as "hte" it sounds quite different ("@" +// vs "ht") and goes down in the list. +// Used when 'spellsuggest' is set to "best". +#define RESCORE(word_score, sound_score) ((3 * (word_score) + (sound_score)) / 4) + +// Do the opposite: based on a maximum end score and a known sound score, +// compute the maximum word score that can be used. +#define MAXSCORE(word_score, sound_score) ((4 * (word_score) - (sound_score)) / 3) + +// only used for su_badflags +#define WF_MIXCAP 0x20 // mix of upper and lower case: macaRONI + +/// Information used when looking for suggestions. +typedef struct suginfo_S { + garray_T su_ga; ///< suggestions, contains "suggest_T" + int su_maxcount; ///< max. number of suggestions displayed + int su_maxscore; ///< maximum score for adding to su_ga + int su_sfmaxscore; ///< idem, for when doing soundfold words + garray_T su_sga; ///< like su_ga, sound-folded scoring + char_u *su_badptr; ///< start of bad word in line + int su_badlen; ///< length of detected bad word in line + int su_badflags; ///< caps flags for bad word + char_u su_badword[MAXWLEN]; ///< bad word truncated at su_badlen + char_u su_fbadword[MAXWLEN]; ///< su_badword case-folded + char_u su_sal_badword[MAXWLEN]; ///< su_badword soundfolded + hashtab_T su_banned; ///< table with banned words + slang_T *su_sallang; ///< default language for sound folding +} suginfo_T; + +/// One word suggestion. Used in "si_ga". +typedef struct { + char_u *st_word; ///< suggested word, allocated string + int st_wordlen; ///< STRLEN(st_word) + int st_orglen; ///< length of replaced text + int st_score; ///< lower is better + int st_altscore; ///< used when st_score compares equal + bool st_salscore; ///< st_score is for soundalike + bool st_had_bonus; ///< bonus already included in score + slang_T *st_slang; ///< language used for sound folding +} suggest_T; + +#define SUG(ga, i) (((suggest_T *)(ga).ga_data)[i]) + +// True if a word appears in the list of banned words. +#define WAS_BANNED(su, word) (!HASHITEM_EMPTY(hash_find(&(su)->su_banned, word))) + +// Number of suggestions kept when cleaning up. We need to keep more than +// what is displayed, because when rescore_suggestions() is called the score +// may change and wrong suggestions may be removed later. +#define SUG_CLEAN_COUNT(su) ((su)->su_maxcount < \ + 130 ? 150 : (su)->su_maxcount + 20) + +// Threshold for sorting and cleaning up suggestions. Don't want to keep lots +// of suggestions that are not going to be displayed. +#define SUG_MAX_COUNT(su) (SUG_CLEAN_COUNT(su) + 50) + +// score for various changes +#define SCORE_SPLIT 149 // split bad word +#define SCORE_SPLIT_NO 249 // split bad word with NOSPLITSUGS +#define SCORE_ICASE 52 // slightly different case +#define SCORE_REGION 200 // word is for different region +#define SCORE_RARE 180 // rare word +#define SCORE_SWAP 75 // swap two characters +#define SCORE_SWAP3 110 // swap two characters in three +#define SCORE_REP 65 // REP replacement +#define SCORE_SUBST 93 // substitute a character +#define SCORE_SIMILAR 33 // substitute a similar character +#define SCORE_SUBCOMP 33 // substitute a composing character +#define SCORE_DEL 94 // delete a character +#define SCORE_DELDUP 66 // delete a duplicated character +#define SCORE_DELCOMP 28 // delete a composing character +#define SCORE_INS 96 // insert a character +#define SCORE_INSDUP 67 // insert a duplicate character +#define SCORE_INSCOMP 30 // insert a composing character +#define SCORE_NONWORD 103 // change non-word to word char + +#define SCORE_FILE 30 // suggestion from a file +#define SCORE_MAXINIT 350 // Initial maximum score: higher == slower. + // 350 allows for about three changes. + +#define SCORE_COMMON1 30 // subtracted for words seen before +#define SCORE_COMMON2 40 // subtracted for words often seen +#define SCORE_COMMON3 50 // subtracted for words very often seen +#define SCORE_THRES2 10 // word count threshold for COMMON2 +#define SCORE_THRES3 100 // word count threshold for COMMON3 + +// When trying changed soundfold words it becomes slow when trying more than +// two changes. With less than two changes it's slightly faster but we miss a +// few good suggestions. In rare cases we need to try three of four changes. +#define SCORE_SFMAX1 200 // maximum score for first try +#define SCORE_SFMAX2 300 // maximum score for second try +#define SCORE_SFMAX3 400 // maximum score for third try + +#define SCORE_BIG (SCORE_INS * 3) // big difference +#define SCORE_MAXMAX 999999 // accept any score +#define SCORE_LIMITMAX 350 // for spell_edit_score_limit() + +// for spell_edit_score_limit() we need to know the minimum value of +// SCORE_ICASE, SCORE_SWAP, SCORE_DEL, SCORE_SIMILAR and SCORE_INS +#define SCORE_EDIT_MIN SCORE_SIMILAR + +/// For finding suggestions: At each node in the tree these states are tried: +typedef enum { + STATE_START = 0, ///< At start of node check for NUL bytes (goodword + ///< ends); if badword ends there is a match, otherwise + ///< try splitting word. + STATE_NOPREFIX, ///< try without prefix + STATE_SPLITUNDO, ///< Undo splitting. + STATE_ENDNUL, ///< Past NUL bytes at start of the node. + STATE_PLAIN, ///< Use each byte of the node. + STATE_DEL, ///< Delete a byte from the bad word. + STATE_INS_PREP, ///< Prepare for inserting bytes. + STATE_INS, ///< Insert a byte in the bad word. + STATE_SWAP, ///< Swap two bytes. + STATE_UNSWAP, ///< Undo swap two characters. + STATE_SWAP3, ///< Swap two characters over three. + STATE_UNSWAP3, ///< Undo Swap two characters over three. + STATE_UNROT3L, ///< Undo rotate three characters left + STATE_UNROT3R, ///< Undo rotate three characters right + STATE_REP_INI, ///< Prepare for using REP items. + STATE_REP, ///< Use matching REP items from the .aff file. + STATE_REP_UNDO, ///< Undo a REP item replacement. + STATE_FINAL, ///< End of this node. +} state_T; + +/// Struct to keep the state at each level in suggest_try_change(). +typedef struct trystate_S { + state_T ts_state; ///< state at this level, STATE_ + int ts_score; ///< score + idx_T ts_arridx; ///< index in tree array, start of node + int16_t ts_curi; ///< index in list of child nodes + char_u ts_fidx; ///< index in fword[], case-folded bad word + char_u ts_fidxtry; ///< ts_fidx at which bytes may be changed + char_u ts_twordlen; ///< valid length of tword[] + char_u ts_prefixdepth; ///< stack depth for end of prefix or + ///< PFD_PREFIXTREE or PFD_NOPREFIX + char_u ts_flags; ///< TSF_ flags + char_u ts_tcharlen; ///< number of bytes in tword character + char_u ts_tcharidx; ///< current byte index in tword character + char_u ts_isdiff; ///< DIFF_ values + char_u ts_fcharstart; ///< index in fword where badword char started + char_u ts_prewordlen; ///< length of word in "preword[]" + char_u ts_splitoff; ///< index in "tword" after last split + char_u ts_splitfidx; ///< "ts_fidx" at word split + char_u ts_complen; ///< nr of compound words used + char_u ts_compsplit; ///< index for "compflags" where word was spit + char_u ts_save_badflags; ///< su_badflags saved here + char_u ts_delidx; ///< index in fword for char that was deleted, + ///< valid when "ts_flags" has TSF_DIDDEL +} trystate_T; + +// values for ts_isdiff +#define DIFF_NONE 0 // no different byte (yet) +#define DIFF_YES 1 // different byte found +#define DIFF_INSERT 2 // inserting character + +// values for ts_flags +#define TSF_PREFIXOK 1 // already checked that prefix is OK +#define TSF_DIDSPLIT 2 // tried split at this point +#define TSF_DIDDEL 4 // did a delete, "ts_delidx" has index + +// special values ts_prefixdepth +#define PFD_NOPREFIX 0xff // not using prefixes +#define PFD_PREFIXTREE 0xfe // walking through the prefix tree +#define PFD_NOTSPECIAL 0xfd // highest value that's not special + +static long spell_suggest_timeout = 5000; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "spellsuggest.c.generated.h" +#endif + +/// Returns true when the sequence of flags in "compflags" plus "flag" can +/// possibly form a valid compounded word. This also checks the COMPOUNDRULE +/// lines if they don't contain wildcards. +static bool can_be_compound(trystate_T *sp, slang_T *slang, char_u *compflags, int flag) +{ + // If the flag doesn't appear in sl_compstartflags or sl_compallflags + // then it can't possibly compound. + if (!byte_in_str(sp->ts_complen == sp->ts_compsplit + ? slang->sl_compstartflags : slang->sl_compallflags, flag)) { + return false; + } + + // If there are no wildcards, we can check if the flags collected so far + // possibly can form a match with COMPOUNDRULE patterns. This only + // makes sense when we have two or more words. + if (slang->sl_comprules != NULL && sp->ts_complen > sp->ts_compsplit) { + compflags[sp->ts_complen] = (char_u)flag; + compflags[sp->ts_complen + 1] = NUL; + bool v = match_compoundrule(slang, compflags + sp->ts_compsplit); + compflags[sp->ts_complen] = NUL; + return v; + } + + return true; +} + +/// Adjust the score of common words. +/// +/// @param split word was split, less bonus +static int score_wordcount_adj(slang_T *slang, int score, char_u *word, bool split) +{ + wordcount_T *wc; + int bonus; + int newscore; + + hashitem_T *hi = hash_find(&slang->sl_wordcount, (char *)word); + if (!HASHITEM_EMPTY(hi)) { + wc = HI2WC(hi); + if (wc->wc_count < SCORE_THRES2) { + bonus = SCORE_COMMON1; + } else if (wc->wc_count < SCORE_THRES3) { + bonus = SCORE_COMMON2; + } else { + bonus = SCORE_COMMON3; + } + if (split) { + newscore = score - bonus / 2; + } else { + newscore = score - bonus; + } + if (newscore < 0) { + return 0; + } + return newscore; + } + return score; +} + +/// Like captype() but for a KEEPCAP word add ONECAP if the word starts with a +/// capital. So that make_case_word() can turn WOrd into Word. +/// Add ALLCAP for "WOrD". +static int badword_captype(char_u *word, char_u *end) + FUNC_ATTR_NONNULL_ALL +{ + int flags = captype(word, end); + int c; + int l, u; + bool first; + char_u *p; + + if (flags & WF_KEEPCAP) { + // Count the number of UPPER and lower case letters. + l = u = 0; + first = false; + for (p = word; p < end; MB_PTR_ADV(p)) { + c = utf_ptr2char((char *)p); + if (SPELL_ISUPPER(c)) { + u++; + if (p == word) { + first = true; + } + } else { + l++; + } + } + + // If there are more UPPER than lower case letters suggest an + // ALLCAP word. Otherwise, if the first letter is UPPER then + // suggest ONECAP. Exception: "ALl" most likely should be "All", + // require three upper case letters. + if (u > l && u > 2) { + flags |= WF_ALLCAP; + } else if (first) { + flags |= WF_ONECAP; + } + + if (u >= 2 && l >= 2) { // maCARONI maCAroni + flags |= WF_MIXCAP; + } + } + return flags; +} + +/// Opposite of offset2bytes(). +/// "pp" points to the bytes and is advanced over it. +/// +/// @return the offset. +static int bytes2offset(char_u **pp) +{ + char_u *p = *pp; + int nr; + int c; + + c = *p++; + if ((c & 0x80) == 0x00) { // 1 byte + nr = c - 1; + } else if ((c & 0xc0) == 0x80) { // 2 bytes + nr = (c & 0x3f) - 1; + nr = nr * 255 + (*p++ - 1); + } else if ((c & 0xe0) == 0xc0) { // 3 bytes + nr = (c & 0x1f) - 1; + nr = nr * 255 + (*p++ - 1); + nr = nr * 255 + (*p++ - 1); + } else { // 4 bytes + nr = (c & 0x0f) - 1; + nr = nr * 255 + (*p++ - 1); + nr = nr * 255 + (*p++ - 1); + nr = nr * 255 + (*p++ - 1); + } + + *pp = p; + return nr; +} + +// values for sps_flags +#define SPS_BEST 1 +#define SPS_FAST 2 +#define SPS_DOUBLE 4 + +static int sps_flags = SPS_BEST; ///< flags from 'spellsuggest' +static int sps_limit = 9999; ///< max nr of suggestions given + +/// Check the 'spellsuggest' option. Return FAIL if it's wrong. +/// Sets "sps_flags" and "sps_limit". +int spell_check_sps(void) +{ + char *p; + char *s; + char_u buf[MAXPATHL]; + int f; + + sps_flags = 0; + sps_limit = 9999; + + for (p = (char *)p_sps; *p != NUL;) { + copy_option_part(&p, (char *)buf, MAXPATHL, ","); + + f = 0; + if (ascii_isdigit(*buf)) { + s = (char *)buf; + sps_limit = getdigits_int(&s, true, 0); + if (*s != NUL && !ascii_isdigit(*s)) { + f = -1; + } + } else if (STRCMP(buf, "best") == 0) { + f = SPS_BEST; + } else if (STRCMP(buf, "fast") == 0) { + f = SPS_FAST; + } else if (STRCMP(buf, "double") == 0) { + f = SPS_DOUBLE; + } else if (STRNCMP(buf, "expr:", 5) != 0 + && STRNCMP(buf, "file:", 5) != 0 + && (STRNCMP(buf, "timeout:", 8) != 0 + || (!ascii_isdigit(buf[8]) + && !(buf[8] == '-' && ascii_isdigit(buf[9]))))) { + f = -1; + } + + if (f == -1 || (sps_flags != 0 && f != 0)) { + sps_flags = SPS_BEST; + sps_limit = 9999; + return FAIL; + } + if (f != 0) { + sps_flags = f; + } + } + + if (sps_flags == 0) { + sps_flags = SPS_BEST; + } + + return OK; +} + +/// "z=": Find badly spelled word under or after the cursor. +/// Give suggestions for the properly spelled word. +/// In Visual mode use the highlighted word as the bad word. +/// When "count" is non-zero use that suggestion. +void spell_suggest(int count) +{ + char_u *line; + pos_T prev_cursor = curwin->w_cursor; + char_u wcopy[MAXWLEN + 2]; + char_u *p; + int c; + suginfo_T sug; + suggest_T *stp; + int mouse_used; + int need_cap; + int limit; + int selected = count; + int badlen = 0; + int msg_scroll_save = msg_scroll; + const int wo_spell_save = curwin->w_p_spell; + + if (!curwin->w_p_spell) { + did_set_spelllang(curwin); + curwin->w_p_spell = true; + } + + if (*curwin->w_s->b_p_spl == NUL) { + emsg(_(e_no_spell)); + return; + } + + if (VIsual_active) { + // Use the Visually selected text as the bad word. But reject + // a multi-line selection. + if (curwin->w_cursor.lnum != VIsual.lnum) { + vim_beep(BO_SPELL); + return; + } + badlen = (int)curwin->w_cursor.col - (int)VIsual.col; + if (badlen < 0) { + badlen = -badlen; + } else { + curwin->w_cursor.col = VIsual.col; + } + badlen++; + end_visual_mode(); + // Find the start of the badly spelled word. + } else if (spell_move_to(curwin, FORWARD, true, true, NULL) == 0 + || curwin->w_cursor.col > prev_cursor.col) { + // No bad word or it starts after the cursor: use the word under the + // cursor. + curwin->w_cursor = prev_cursor; + line = get_cursor_line_ptr(); + p = line + curwin->w_cursor.col; + // Backup to before start of word. + while (p > line && spell_iswordp_nmw(p, curwin)) { + MB_PTR_BACK(line, p); + } + // Forward to start of word. + while (*p != NUL && !spell_iswordp_nmw(p, curwin)) { + MB_PTR_ADV(p); + } + + if (!spell_iswordp_nmw(p, curwin)) { // No word found. + beep_flush(); + return; + } + curwin->w_cursor.col = (colnr_T)(p - line); + } + + // Get the word and its length. + + // Figure out if the word should be capitalised. + need_cap = check_need_cap(curwin->w_cursor.lnum, curwin->w_cursor.col); + + // Make a copy of current line since autocommands may free the line. + line = vim_strsave(get_cursor_line_ptr()); + spell_suggest_timeout = 5000; + + // Get the list of suggestions. Limit to 'lines' - 2 or the number in + // 'spellsuggest', whatever is smaller. + if (sps_limit > Rows - 2) { + limit = Rows - 2; + } else { + limit = sps_limit; + } + spell_find_suggest(line + curwin->w_cursor.col, badlen, &sug, limit, + true, need_cap, true); + + if (GA_EMPTY(&sug.su_ga)) { + msg(_("Sorry, no suggestions")); + } else if (count > 0) { + if (count > sug.su_ga.ga_len) { + smsg(_("Sorry, only %" PRId64 " suggestions"), + (int64_t)sug.su_ga.ga_len); + } + } else { + // When 'rightleft' is set the list is drawn right-left. + cmdmsg_rl = curwin->w_p_rl; + if (cmdmsg_rl) { + msg_col = Columns - 1; + } + + // List the suggestions. + msg_start(); + msg_row = Rows - 1; // for when 'cmdheight' > 1 + lines_left = Rows; // avoid more prompt + vim_snprintf((char *)IObuff, IOSIZE, _("Change \"%.*s\" to:"), + sug.su_badlen, sug.su_badptr); + if (cmdmsg_rl && STRNCMP(IObuff, "Change", 6) == 0) { + // And now the rabbit from the high hat: Avoid showing the + // untranslated message rightleft. + vim_snprintf((char *)IObuff, IOSIZE, ":ot \"%.*s\" egnahC", + sug.su_badlen, sug.su_badptr); + } + msg_puts((const char *)IObuff); + msg_clr_eos(); + msg_putchar('\n'); + + msg_scroll = true; + for (int i = 0; i < sug.su_ga.ga_len; i++) { + stp = &SUG(sug.su_ga, i); + + // The suggested word may replace only part of the bad word, add + // the not replaced part. But only when it's not getting too long. + STRLCPY(wcopy, stp->st_word, MAXWLEN + 1); + int el = sug.su_badlen - stp->st_orglen; + if (el > 0 && stp->st_wordlen + el <= MAXWLEN) { + STRLCPY(wcopy + stp->st_wordlen, sug.su_badptr + stp->st_orglen, el + 1); + } + vim_snprintf((char *)IObuff, IOSIZE, "%2d", i + 1); + if (cmdmsg_rl) { + rl_mirror(IObuff); + } + msg_puts((const char *)IObuff); + + vim_snprintf((char *)IObuff, IOSIZE, " \"%s\"", wcopy); + msg_puts((const char *)IObuff); + + // The word may replace more than "su_badlen". + if (sug.su_badlen < stp->st_orglen) { + vim_snprintf((char *)IObuff, IOSIZE, _(" < \"%.*s\""), + stp->st_orglen, sug.su_badptr); + msg_puts((const char *)IObuff); + } + + if (p_verbose > 0) { + // Add the score. + if (sps_flags & (SPS_DOUBLE | SPS_BEST)) { + vim_snprintf((char *)IObuff, IOSIZE, " (%s%d - %d)", + stp->st_salscore ? "s " : "", + stp->st_score, stp->st_altscore); + } else { + vim_snprintf((char *)IObuff, IOSIZE, " (%d)", + stp->st_score); + } + if (cmdmsg_rl) { + // Mirror the numbers, but keep the leading space. + rl_mirror(IObuff + 1); + } + msg_advance(30); + msg_puts((const char *)IObuff); + } + msg_putchar('\n'); + } + + cmdmsg_rl = false; + msg_col = 0; + // Ask for choice. + selected = prompt_for_number(&mouse_used); + + if (ui_has(kUIMessages)) { + ui_call_msg_clear(); + } + + if (mouse_used) { + selected -= lines_left; + } + lines_left = Rows; // avoid more prompt + // don't delay for 'smd' in normal_cmd() + msg_scroll = msg_scroll_save; + } + + if (selected > 0 && selected <= sug.su_ga.ga_len && u_save_cursor() == OK) { + // Save the from and to text for :spellrepall. + XFREE_CLEAR(repl_from); + XFREE_CLEAR(repl_to); + + stp = &SUG(sug.su_ga, selected - 1); + if (sug.su_badlen > stp->st_orglen) { + // Replacing less than "su_badlen", append the remainder to + // repl_to. + repl_from = vim_strnsave(sug.su_badptr, (size_t)sug.su_badlen); + vim_snprintf((char *)IObuff, IOSIZE, "%s%.*s", stp->st_word, + sug.su_badlen - stp->st_orglen, + sug.su_badptr + stp->st_orglen); + repl_to = vim_strsave(IObuff); + } else { + // Replacing su_badlen or more, use the whole word. + repl_from = vim_strnsave(sug.su_badptr, (size_t)stp->st_orglen); + repl_to = vim_strsave(stp->st_word); + } + + // Replace the word. + p = xmalloc(STRLEN(line) - (size_t)stp->st_orglen + (size_t)stp->st_wordlen + 1); + c = (int)(sug.su_badptr - line); + memmove(p, line, (size_t)c); + STRCPY(p + c, stp->st_word); + STRCAT(p, sug.su_badptr + stp->st_orglen); + + // For redo we use a change-word command. + ResetRedobuff(); + AppendToRedobuff("ciw"); + AppendToRedobuffLit((char *)p + c, + stp->st_wordlen + sug.su_badlen - stp->st_orglen); + AppendCharToRedobuff(ESC); + + // "p" may be freed here + ml_replace(curwin->w_cursor.lnum, (char *)p, false); + curwin->w_cursor.col = c; + + inserted_bytes(curwin->w_cursor.lnum, c, stp->st_orglen, stp->st_wordlen); + } else { + curwin->w_cursor = prev_cursor; + } + + spell_find_cleanup(&sug); + xfree(line); + curwin->w_p_spell = wo_spell_save; +} + +/// Find spell suggestions for "word". Return them in the growarray "*gap" as +/// a list of allocated strings. +/// +/// @param maxcount maximum nr of suggestions +/// @param need_cap 'spellcapcheck' matched +void spell_suggest_list(garray_T *gap, char_u *word, int maxcount, bool need_cap, bool interactive) +{ + suginfo_T sug; + suggest_T *stp; + char_u *wcopy; + + spell_find_suggest(word, 0, &sug, maxcount, false, need_cap, interactive); + + // Make room in "gap". + ga_init(gap, sizeof(char_u *), sug.su_ga.ga_len + 1); + ga_grow(gap, sug.su_ga.ga_len); + for (int i = 0; i < sug.su_ga.ga_len; i++) { + stp = &SUG(sug.su_ga, i); + + // The suggested word may replace only part of "word", add the not + // replaced part. + wcopy = xmalloc((size_t)stp->st_wordlen + STRLEN(sug.su_badptr + stp->st_orglen) + 1); + STRCPY(wcopy, stp->st_word); + STRCPY(wcopy + stp->st_wordlen, sug.su_badptr + stp->st_orglen); + ((char_u **)gap->ga_data)[gap->ga_len++] = wcopy; + } + + spell_find_cleanup(&sug); +} + +/// Find spell suggestions for the word at the start of "badptr". +/// Return the suggestions in "su->su_ga". +/// The maximum number of suggestions is "maxcount". +/// Note: does use info for the current window. +/// This is based on the mechanisms of Aspell, but completely reimplemented. +/// +/// @param badlen length of bad word or 0 if unknown +/// @param banbadword don't include badword in suggestions +/// @param need_cap word should start with capital +static void spell_find_suggest(char_u *badptr, int badlen, suginfo_T *su, int maxcount, + bool banbadword, bool need_cap, bool interactive) +{ + hlf_T attr = HLF_COUNT; + char_u buf[MAXPATHL]; + char *p; + bool do_combine = false; + char_u *sps_copy; + static bool expr_busy = false; + int c; + langp_T *lp; + bool did_intern = false; + + // Set the info in "*su". + CLEAR_POINTER(su); + ga_init(&su->su_ga, (int)sizeof(suggest_T), 10); + ga_init(&su->su_sga, (int)sizeof(suggest_T), 10); + if (*badptr == NUL) { + return; + } + hash_init(&su->su_banned); + + su->su_badptr = badptr; + if (badlen != 0) { + su->su_badlen = badlen; + } else { + size_t tmplen = spell_check(curwin, su->su_badptr, &attr, NULL, false); + assert(tmplen <= INT_MAX); + su->su_badlen = (int)tmplen; + } + su->su_maxcount = maxcount; + su->su_maxscore = SCORE_MAXINIT; + + if (su->su_badlen >= MAXWLEN) { + su->su_badlen = MAXWLEN - 1; // just in case + } + STRLCPY(su->su_badword, su->su_badptr, su->su_badlen + 1); + (void)spell_casefold(curwin, su->su_badptr, su->su_badlen, su->su_fbadword, + MAXWLEN); + + // TODO(vim): make this work if the case-folded text is longer than the + // original text. Currently an illegal byte causes wrong pointer + // computations. + su->su_fbadword[su->su_badlen] = NUL; + + // get caps flags for bad word + su->su_badflags = badword_captype(su->su_badptr, + su->su_badptr + su->su_badlen); + if (need_cap) { + su->su_badflags |= WF_ONECAP; + } + + // Find the default language for sound folding. We simply use the first + // one in 'spelllang' that supports sound folding. That's good for when + // using multiple files for one language, it's not that bad when mixing + // languages (e.g., "pl,en"). + for (int i = 0; i < curbuf->b_s.b_langp.ga_len; i++) { + lp = LANGP_ENTRY(curbuf->b_s.b_langp, i); + if (lp->lp_sallang != NULL) { + su->su_sallang = lp->lp_sallang; + break; + } + } + + // Soundfold the bad word with the default sound folding, so that we don't + // have to do this many times. + if (su->su_sallang != NULL) { + spell_soundfold(su->su_sallang, su->su_fbadword, true, + su->su_sal_badword); + } + + // If the word is not capitalised and spell_check() doesn't consider the + // word to be bad then it might need to be capitalised. Add a suggestion + // for that. + c = utf_ptr2char((char *)su->su_badptr); + if (!SPELL_ISUPPER(c) && attr == HLF_COUNT) { + make_case_word(su->su_badword, buf, WF_ONECAP); + add_suggestion(su, &su->su_ga, buf, su->su_badlen, SCORE_ICASE, + 0, true, su->su_sallang, false); + } + + // Ban the bad word itself. It may appear in another region. + if (banbadword) { + add_banned(su, su->su_badword); + } + + // Make a copy of 'spellsuggest', because the expression may change it. + sps_copy = vim_strsave(p_sps); + + // Loop over the items in 'spellsuggest'. + for (p = (char *)sps_copy; *p != NUL;) { + copy_option_part(&p, (char *)buf, MAXPATHL, ","); + + if (STRNCMP(buf, "expr:", 5) == 0) { + // Evaluate an expression. Skip this when called recursively, + // when using spellsuggest() in the expression. + if (!expr_busy) { + expr_busy = true; + spell_suggest_expr(su, buf + 5); + expr_busy = false; + } + } else if (STRNCMP(buf, "file:", 5) == 0) { + // Use list of suggestions in a file. + spell_suggest_file(su, buf + 5); + } else if (STRNCMP(buf, "timeout:", 8) == 0) { + // Limit the time searching for suggestions. + spell_suggest_timeout = atol((char *)buf + 8); + } else if (!did_intern) { + // Use internal method once. + spell_suggest_intern(su, interactive); + if (sps_flags & SPS_DOUBLE) { + do_combine = true; + } + did_intern = true; + } + } + + xfree(sps_copy); + + if (do_combine) { + // Combine the two list of suggestions. This must be done last, + // because sorting changes the order again. + score_combine(su); + } +} + +/// Find suggestions by evaluating expression "expr". +static void spell_suggest_expr(suginfo_T *su, char_u *expr) +{ + int score; + const char *p; + + // The work is split up in a few parts to avoid having to export + // suginfo_T. + // First evaluate the expression and get the resulting list. + list_T *const list = eval_spell_expr((char *)su->su_badword, (char *)expr); + if (list != NULL) { + // Loop over the items in the list. + TV_LIST_ITER(list, li, { + if (TV_LIST_ITEM_TV(li)->v_type == VAR_LIST) { + // Get the word and the score from the items. + score = get_spellword(TV_LIST_ITEM_TV(li)->vval.v_list, &p); + if (score >= 0 && score <= su->su_maxscore) { + add_suggestion(su, &su->su_ga, (const char_u *)p, su->su_badlen, + score, 0, true, su->su_sallang, false); + } + } + }); + tv_list_unref(list); + } + + // Remove bogus suggestions, sort and truncate at "maxcount". + check_suggestions(su, &su->su_ga); + (void)cleanup_suggestions(&su->su_ga, su->su_maxscore, su->su_maxcount); +} + +/// Find suggestions in file "fname". Used for "file:" in 'spellsuggest'. +static void spell_suggest_file(suginfo_T *su, char_u *fname) +{ + FILE *fd; + char_u line[MAXWLEN * 2]; + char_u *p; + int len; + char_u cword[MAXWLEN]; + + // Open the file. + fd = os_fopen((char *)fname, "r"); + if (fd == NULL) { + semsg(_(e_notopen), fname); + return; + } + + // Read it line by line. + while (!vim_fgets(line, MAXWLEN * 2, fd) && !got_int) { + line_breakcheck(); + + p = (char_u *)vim_strchr((char *)line, '/'); + if (p == NULL) { + continue; // No Tab found, just skip the line. + } + *p++ = NUL; + if (STRICMP(su->su_badword, line) == 0) { + // Match! Isolate the good word, until CR or NL. + for (len = 0; p[len] >= ' '; len++) {} + p[len] = NUL; + + // If the suggestion doesn't have specific case duplicate the case + // of the bad word. + if (captype(p, NULL) == 0) { + make_case_word(p, cword, su->su_badflags); + p = cword; + } + + add_suggestion(su, &su->su_ga, p, su->su_badlen, + SCORE_FILE, 0, true, su->su_sallang, false); + } + } + + fclose(fd); + + // Remove bogus suggestions, sort and truncate at "maxcount". + check_suggestions(su, &su->su_ga); + (void)cleanup_suggestions(&su->su_ga, su->su_maxscore, su->su_maxcount); +} + +/// Find suggestions for the internal method indicated by "sps_flags". +static void spell_suggest_intern(suginfo_T *su, bool interactive) +{ + // Load the .sug file(s) that are available and not done yet. + suggest_load_files(); + + // 1. Try special cases, such as repeating a word: "the the" -> "the". + // + // Set a maximum score to limit the combination of operations that is + // tried. + suggest_try_special(su); + + // 2. Try inserting/deleting/swapping/changing a letter, use REP entries + // from the .aff file and inserting a space (split the word). + suggest_try_change(su); + + // For the resulting top-scorers compute the sound-a-like score. + if (sps_flags & SPS_DOUBLE) { + score_comp_sal(su); + } + + // 3. Try finding sound-a-like words. + if ((sps_flags & SPS_FAST) == 0) { + if (sps_flags & SPS_BEST) { + // Adjust the word score for the suggestions found so far for how + // they sounds like. + rescore_suggestions(su); + } + + // While going through the soundfold tree "su_maxscore" is the score + // for the soundfold word, limits the changes that are being tried, + // and "su_sfmaxscore" the rescored score, which is set by + // cleanup_suggestions(). + // First find words with a small edit distance, because this is much + // faster and often already finds the top-N suggestions. If we didn't + // find many suggestions try again with a higher edit distance. + // "sl_sounddone" is used to avoid doing the same word twice. + suggest_try_soundalike_prep(); + su->su_maxscore = SCORE_SFMAX1; + su->su_sfmaxscore = SCORE_MAXINIT * 3; + suggest_try_soundalike(su); + if (su->su_ga.ga_len < SUG_CLEAN_COUNT(su)) { + // We didn't find enough matches, try again, allowing more + // changes to the soundfold word. + su->su_maxscore = SCORE_SFMAX2; + suggest_try_soundalike(su); + if (su->su_ga.ga_len < SUG_CLEAN_COUNT(su)) { + // Still didn't find enough matches, try again, allowing even + // more changes to the soundfold word. + su->su_maxscore = SCORE_SFMAX3; + suggest_try_soundalike(su); + } + } + su->su_maxscore = su->su_sfmaxscore; + suggest_try_soundalike_finish(); + } + + // When CTRL-C was hit while searching do show the results. Only clear + // got_int when using a command, not for spellsuggest(). + os_breakcheck(); + if (interactive && got_int) { + (void)vgetc(); + got_int = false; + } + + if ((sps_flags & SPS_DOUBLE) == 0 && su->su_ga.ga_len != 0) { + if (sps_flags & SPS_BEST) { + // Adjust the word score for how it sounds like. + rescore_suggestions(su); + } + + // Remove bogus suggestions, sort and truncate at "maxcount". + check_suggestions(su, &su->su_ga); + (void)cleanup_suggestions(&su->su_ga, su->su_maxscore, su->su_maxcount); + } +} + +/// Free the info put in "*su" by spell_find_suggest(). +static void spell_find_cleanup(suginfo_T *su) +{ +#define FREE_SUG_WORD(sug) xfree((sug)->st_word) + // Free the suggestions. + GA_DEEP_CLEAR(&su->su_ga, suggest_T, FREE_SUG_WORD); + GA_DEEP_CLEAR(&su->su_sga, suggest_T, FREE_SUG_WORD); + + // Free the banned words. + hash_clear_all(&su->su_banned, 0); +} + +/// Try finding suggestions by recognizing specific situations. +static void suggest_try_special(suginfo_T *su) +{ + int c; + char_u word[MAXWLEN]; + + // Recognize a word that is repeated: "the the". + char_u *p = skiptowhite(su->su_fbadword); + size_t len = (size_t)(p - su->su_fbadword); + p = (char_u *)skipwhite((char *)p); + if (STRLEN(p) == len && STRNCMP(su->su_fbadword, p, len) == 0) { + // Include badflags: if the badword is onecap or allcap + // use that for the goodword too: "The the" -> "The". + c = su->su_fbadword[len]; + su->su_fbadword[len] = NUL; + make_case_word(su->su_fbadword, word, su->su_badflags); + su->su_fbadword[len] = (c |