aboutsummaryrefslogtreecommitdiff
path: root/src/nvim/spellsuggest.c
diff options
context:
space:
mode:
authorJosh Rahm <rahm@google.com>2022-08-19 12:26:08 -0600
committerJosh Rahm <rahm@google.com>2022-08-19 13:06:41 -0600
commita7237662f96933efe29eed8212464571e3778cd0 (patch)
tree27930202726b4251437c8cfa53069f65b4db90dc /src/nvim/spellsuggest.c
parent02292344929069ea63c0bb872cc22d552d86b67f (diff)
parentb2f979b30beac67906b2dd717fcb6a34f46f5e54 (diff)
downloadrneovim-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.c3800
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