From 61ee9fde311a3731dab604482b138a9f3ded615d Mon Sep 17 00:00:00 2001
From: Isaac <IsaacOscar@live.com.au>
Date: Tue, 26 Apr 2022 19:14:34 +1200
Subject: [PATCH] Allow removing words from the personal dictionary, that
 weren't previously added.

---
 src/AspellChecker.cpp      | 29 +++++++++++------------
 src/HunspellChecker.cpp    | 22 ++++++++----------
 src/PersonalWordList.cpp   | 21 +++++++++--------
 src/PersonalWordList.h     | 47 +++++++++++++++++++++++++++++++++-----
 src/SpellChecker.h         | 10 --------
 src/frontends/qt/Menus.cpp |  9 ++------
 6 files changed, 76 insertions(+), 62 deletions(-)

diff --git a/src/AspellChecker.cpp b/src/AspellChecker.cpp
index a5c54d8c74..8789b6c789 100644
--- a/src/AspellChecker.cpp
+++ b/src/AspellChecker.cpp
@@ -75,13 +75,12 @@ struct AspellChecker::Private
 	string toAspellWord(docstring const & word) const;
 
 	SpellChecker::Result check(AspellSpeller * m,
-		WordLangTuple const & word) const;
+		WordLangTuple const & word);
 
 	void initSessionDictionary(Speller const & speller, PersonalWordList * pd);
 	void addToSession(AspellCanHaveError * speller, docstring const & word);
 	void insert(WordLangTuple const & word);
 	void remove(WordLangTuple const & word);
-	bool learned(WordLangTuple const & word);
 
 	void accept(Speller & speller, WordLangTuple const & word);
 
@@ -238,8 +237,8 @@ void AspellChecker::Private::initSessionDictionary(
 {
 	AspellSpeller * aspell = to_aspell_speller(speller.e_speller);
 	aspell_speller_clear_session(aspell);
-	docstring_list::const_iterator it = pd->begin();
-	docstring_list::const_iterator et = pd->end();
+	docstring_list::const_iterator it = pd->includes_begin();
+	docstring_list::const_iterator et = pd->includes_end();
 	for (; it != et; ++it) {
 		addToSession(speller.e_speller, *it);
 	}
@@ -352,8 +351,8 @@ string AspellChecker::Private::toAspellWord(docstring const & word) const
 
 SpellChecker::Result AspellChecker::Private::check(
 	AspellSpeller * m, WordLangTuple const & word)
-	const
 {
+	PersonalWordList * pd = personal_[word.lang()->lang()];
 	SpellChecker::Result result = WORD_OK;
 	docstring w1;
 	LYXERR(Debug::GUI, "spellCheck: \"" <<
@@ -364,6 +363,9 @@ SpellChecker::Result AspellChecker::Private::check(
 		int const word_ok = aspell_speller_check(m, word_str.c_str(), -1);
 		LASSERT(word_ok != -1, return UNKNOWN_WORD);
 		result = (word_ok) ? WORD_OK : UNKNOWN_WORD;
+		if (result == WORD_OK && pd && pd->excluded(w1)) {
+			result = UNKNOWN_WORD;
+		}
 		if (rest.empty())
 			break;
 		rest = split(rest, w1, '-');
@@ -373,7 +375,11 @@ SpellChecker::Result AspellChecker::Private::check(
 	string const word_str = toAspellWord(word.word());
 	int const word_ok = aspell_speller_check(m, word_str.c_str(), -1);
 	LASSERT(word_ok != -1, return UNKNOWN_WORD);
-	return (word_ok) ? WORD_OK : UNKNOWN_WORD;
+	result = (word_ok) ? WORD_OK : UNKNOWN_WORD;
+	if (result == WORD_OK && pd && pd->excluded(word.word())) {
+		result = UNKNOWN_WORD;
+	}
+	return result;
 }
 
 void AspellChecker::Private::accept(Speller & speller, WordLangTuple const & word)
@@ -408,14 +414,6 @@ void AspellChecker::Private::insert(WordLangTuple const & word)
 	}
 }
 
-bool AspellChecker::Private::learned(WordLangTuple const & word)
-{
-	PersonalWordList * pd = personal_[word.lang()->lang()];
-	if (!pd)
-		return false;
-	return pd->exists(word.word());
-}
-
 
 AspellChecker::AspellChecker()
 	: d(new Private)
@@ -447,8 +445,7 @@ SpellChecker::Result AspellChecker::check(WordLangTuple const & word,
 		if (it->word() == word.word())
 			return DOCUMENT_LEARNED_WORD;
 	}
-	SpellChecker::Result rc = d->check(m, word);
-	return (rc == WORD_OK && d->learned(word)) ? LEARNED_WORD : rc;
+	return d->check(m, word);
 }
 
 
diff --git a/src/HunspellChecker.cpp b/src/HunspellChecker.cpp
index 01ec19498b..fa9ec9c203 100644
--- a/src/HunspellChecker.cpp
+++ b/src/HunspellChecker.cpp
@@ -72,7 +72,6 @@ struct HunspellChecker::Private
 	/// personal word list interface
 	void remove(WordLangTuple const & wl);
 	void insert(WordLangTuple const & wl);
-	bool learned(WordLangTuple const & wl);
 	/// the spellers
 	Spellers spellers_;
 	///
@@ -256,12 +255,18 @@ Hunspell * HunspellChecker::Private::addSpeller(Language const * lang)
 		PersonalWordList * pd = new PersonalWordList(lang->lang());
 		pd->load();
 		personal_[lang->lang()] = pd;
-		docstring_list::const_iterator it = pd->begin();
-		docstring_list::const_iterator et = pd->end();
+		docstring_list::const_iterator it = pd->includes_begin();
+		docstring_list::const_iterator et = pd->includes_end();
 		for (; it != et; ++it) {
 			string const word_to_add = to_iconv_encoding(*it, encoding);
 			h->add(word_to_add.c_str());
 		}
+		it = pd->excludes_begin();
+		et = pd->excludes_end();
+		for (; it != et; ++it) {
+			string const word_to_add = to_iconv_encoding(*it, encoding);
+			h->remove(word_to_add.c_str());
+		}
 	}
 	return h;
 }
@@ -322,15 +327,6 @@ void HunspellChecker::Private::insert(WordLangTuple const & wl)
 }
 
 
-bool HunspellChecker::Private::learned(WordLangTuple const & wl)
-{
-	PersonalWordList * pd = personal_[wl.lang()->lang()];
-	if (!pd)
-		return false;
-	return pd->exists(wl.word());
-}
-
-
 HunspellChecker::HunspellChecker()
 	: d(new Private)
 {}
@@ -371,7 +367,7 @@ SpellChecker::Result HunspellChecker::check(WordLangTuple const & wl,
 #else
 	if (h->spell(word_to_check.c_str(), &info))
 #endif
-		return d->learned(wl) ? LEARNED_WORD : WORD_OK;
+		return WORD_OK;
 
 	if (info & SPELL_COMPOUND) {
 		// FIXME: What to do with that?
diff --git a/src/PersonalWordList.cpp b/src/PersonalWordList.cpp
index ca61d8a171..5a34a28e83 100644
--- a/src/PersonalWordList.cpp
+++ b/src/PersonalWordList.cpp
@@ -26,26 +26,27 @@ using namespace lyx::support;
 
 namespace lyx {
 
-FileName PersonalWordList::dictfile() const
+FileName PersonalWordListPart::dictfile() const
 {
-	string fname = "pwl_" + lang_ + ".dict";
+	string fext = is_includes_ ? ".dict" : ".excl";
+	string fname = "pwl_" + lang_ + fext;
 	return FileName(addName(package().user_support().absFileName(),fname));
 }
 
 
-docstring_list::const_iterator PersonalWordList::begin() const
+docstring_list::const_iterator PersonalWordListPart::begin() const
 {
 	return words_.begin();
 }
 
 
-docstring_list::const_iterator PersonalWordList::end() const
+docstring_list::const_iterator PersonalWordListPart::end() const
 {
 	return words_.end();
 }
 
 
-void PersonalWordList::load()
+void PersonalWordListPart::load()
 {
 	FileName fn = dictfile();
 	LYXERR(Debug::FILES, "load personal dictionary from: " << fn);
@@ -72,7 +73,7 @@ void PersonalWordList::load()
 }
 
 
-void PersonalWordList::save()
+void PersonalWordListPart::save()
 {
 	if (!isDirty())
 		return;
@@ -90,13 +91,13 @@ void PersonalWordList::save()
 }
 
 
-bool PersonalWordList::equalwords(docstring const & w1, docstring const & w2) const
+bool PersonalWordListPart::equalwords(docstring const & w1, docstring const & w2) const
 {
 	return w1 == w2;
 }
 
 
-bool PersonalWordList::exists(docstring const & word) const
+bool PersonalWordListPart::exists(docstring const & word) const
 {
 	docstring_list::const_iterator it = words_.begin();
 	docstring_list::const_iterator et = words_.end();
@@ -108,7 +109,7 @@ bool PersonalWordList::exists(docstring const & word) const
 }
 
 
-void PersonalWordList::insert(docstring const & word)
+void PersonalWordListPart::insert(docstring const & word)
 {
 	if (exists(word))
 		return;
@@ -117,7 +118,7 @@ void PersonalWordList::insert(docstring const & word)
 }
 
 
-void PersonalWordList::remove(docstring const & word)
+void PersonalWordListPart::remove(docstring const & word)
 {
 	docstring_list::iterator it = words_.begin();
 	docstring_list::const_iterator et = words_.end();
diff --git a/src/PersonalWordList.h b/src/PersonalWordList.h
index 0194bbcbe6..a7217f2b5b 100644
--- a/src/PersonalWordList.h
+++ b/src/PersonalWordList.h
@@ -20,11 +20,10 @@
 
 namespace lyx {
 
-/// A PersonalWordList holds a word list with persistent state
-class PersonalWordList {
-public:
-	/// the word list has an associated language
-	PersonalWordList(std::string const & lang) : lang_(lang), dirty_(false) {}
+/// A PersonalWordListPart holds a part of the word list with persistent state
+struct PersonalWordListPart {
+	/// the word list has an associated language, and a flag indicating whether it is an includes or excludes list
+	PersonalWordListPart(std::string lang, bool is_includes) : lang_(lang), is_includes_(is_includes), dirty_(false) {}
 	/// the location of the file to hold to word list
 	lyx::support::FileName dictfile() const;
 	/// (re)load the word list from file
@@ -49,15 +48,51 @@ private:
 	///
 	std::string lang_;
 	///
+	bool is_includes_;
+	///
 	bool dirty_;
 	///
 	bool equalwords(docstring const & w1, docstring const & w2) const;
 	///
-	std::string header() const { return "# personal word list"; }
+	std::string header() const { return is_includes_ ? "# personal word list" : "# personal world list (exclusions)";  }
 	///
 	void dirty(bool flag) { dirty_ = flag; }
 };
 
+/// A PersonalWordState holds a list of words to include (i.e. marked as spelt correctly), and a list of words to exclude (i.e. marked as spelled incorrectly)
+class PersonalWordList {
+public:
+	/// the word list has an associated language
+	PersonalWordList(std::string lang) : includes_(lang, true), excludes_(lang, false) {}
+
+	/// first item in includes word list
+	docstring_list::const_iterator includes_begin() const { return includes_.begin(); }
+	/// end of includes word list
+	docstring_list::const_iterator includes_end() const { return includes_.end(); }
+	/// first item in excludes word list
+	docstring_list::const_iterator excludes_begin() const { return excludes_.begin(); }
+	/// end of excludes word list
+	docstring_list::const_iterator excludes_end() const { return excludes_.end(); }
+	/// (re)load both word lists from file
+	void load() { includes_.load(); excludes_.load(); }
+	/// save both word lists to file
+	void save() { includes_.save(); excludes_.save(); }
+	/// is the given word excluded? (i.e. we previously called remove)
+	bool excluded(docstring const & word) const { return excludes_.exists(word); }
+	/// is the given word included? (i.e. we previously called insert)
+	bool included(docstring const & word) const { return includes_.exists(word); }
+	/// insert a given word to the set of valid words
+	void insert(docstring const & word) { excludes_.remove(word); includes_.insert(word); }
+	/// remove given word from the set of valid words
+	void remove(docstring const & word) { includes_.remove(word); excludes_.insert(word); }
+
+private:
+	/// The list of words to include
+	PersonalWordListPart includes_;
+	/// The list of words to exclude
+	PersonalWordListPart excludes_;
+};
+
 } // namespace lyx
 
 #endif // PERSONAL_WORD_LIST_H
diff --git a/src/SpellChecker.h b/src/SpellChecker.h
index 58e5de6bd2..e3c65707b6 100644
--- a/src/SpellChecker.h
+++ b/src/SpellChecker.h
@@ -34,16 +34,8 @@ public:
 	enum Result  {
 		/// word is correct
 		WORD_OK = 1,
-		/// root of given word was found
-		ROOT_FOUND,
-		/// word found through compound formation
-		COMPOUND_WORD,
 		/// word not found
 		UNKNOWN_WORD,
-		/// number of other ignored "word"
-		IGNORED_WORD,
-		/// number of personal dictionary "word"
-		LEARNED_WORD,
 		/// number of document dictionary "word"
 		DOCUMENT_LEARNED_WORD,
 		/// missing dictionary for language
@@ -57,9 +49,7 @@ public:
 	/// does the spell check failed
 	static bool misspelled(Result res) {
 		return res != WORD_OK
-			&& res != IGNORED_WORD
 			&& res != NO_DICTIONARY
-			&& res != LEARNED_WORD
 			&& res != DOCUMENT_LEARNED_WORD; }
 
 	/// check the given word of the given lang code and return the result
diff --git a/src/frontends/qt/Menus.cpp b/src/frontends/qt/Menus.cpp
index 2fbac3f2c5..c003fda93f 100644
--- a/src/frontends/qt/Menus.cpp
+++ b/src/frontends/qt/Menus.cpp
@@ -880,8 +880,8 @@ void MenuDefinition::expandSpellingSuggestions(BufferView const * bv)
 			}
 		}
 		break;
-	case SpellChecker::LEARNED_WORD: {
-			LYXERR(Debug::GUI, "Learned Word.");
+	case SpellChecker::WORD_OK: {
+			LYXERR(Debug::GUI, "Valid Word.");
 			docstring const arg = wl.word() + " " + from_ascii(wl.lang()->lang());
 			add(MenuItem(MenuItem::Command, qt_("Remove from personal dictionary|r"),
 					FuncRequest(LFUN_SPELLING_REMOVE, arg)));
@@ -897,11 +897,6 @@ void MenuDefinition::expandSpellingSuggestions(BufferView const * bv)
 	case SpellChecker::NO_DICTIONARY:
 		LYXERR(Debug::GUI, "No dictionary for language " + from_ascii(wl.lang()->lang()));
 		// FALLTHROUGH
-	case SpellChecker::WORD_OK:
-	case SpellChecker::COMPOUND_WORD:
-	case SpellChecker::ROOT_FOUND:
-	case SpellChecker::IGNORED_WORD:
-		break;
 	}
 }
 
-- 
2.36.0

