From 2caec486443ea86fc735be993f1eff457570e3f6 Mon Sep 17 00:00:00 2001
From: John Zimmermann <me@johnnynator.dev>
Date: Sat, 3 Aug 2024 17:47:00 +0200
Subject: [PATCH 3/3] Replace ispell with Sonnet

---
 linphone-app/CMakeLists.txt                   |  12 +-
 .../other/spell-checker/SpellChecker.hpp      |  34 ++--
 .../other/spell-checker/SpellCheckerLinux.cpp | 186 ++----------------
 3 files changed, 26 insertions(+), 206 deletions(-)

diff --git a/linphone-app/CMakeLists.txt b/linphone-app/CMakeLists.txt
index d2b5dcde..770234db 100644
--- a/linphone-app/CMakeLists.txt
+++ b/linphone-app/CMakeLists.txt
@@ -126,13 +126,7 @@ if(ENABLE_QT_KEYCHAIN)
 endif()
 
 if(NOT APPLE AND NOT WIN32)
-	if(NOT ISPELL_TARGET_NAME)
-		set(ISPELL_TARGET_NAME "ISpell")
-	endif()
-	find_package(${ISPELL_TARGET_NAME})
-	if(NOT ISpell_FOUND)
-		find_package(${ISPELL_TARGET_NAME} CONFIG REQUIRED)
-	endif()
+	find_package(KF5Sonnet REQUIRED)
 endif()
 
 if(ENABLE_BUILD_VERBOSE)
@@ -198,7 +192,7 @@ if( ENABLE_QT_KEYCHAIN)
 	list(APPEND APP_TARGETS ${QTKEYCHAIN_TARGET_NAME})
 endif()
 if(NOT APPLE AND NOT WIN32)
-	list(APPEND APP_TARGETS ${ISPELL_TARGET_NAME})
+	list(APPEND APP_TARGETS KF5::SonnetCore)
 endif()
 if (UNIX AND NOT APPLE)
 	list(APPEND QT5_PACKAGES DBus)
diff --git a/linphone-app/src/components/other/spell-checker/SpellChecker.hpp b/linphone-app/src/components/other/spell-checker/SpellChecker.hpp
index 84d9abdf..37a1dd8b 100644
--- a/linphone-app/src/components/other/spell-checker/SpellChecker.hpp
+++ b/linphone-app/src/components/other/spell-checker/SpellChecker.hpp
@@ -39,7 +39,7 @@
 #include "components/settings/SettingsModel.hpp"
 
 #ifdef __linux__
-#include <thread>
+#include <Sonnet/Speller>
 #endif
 
 #define SUGGESTIONS_LIMIT 10
@@ -57,7 +57,7 @@ class SpellChecker : public QSyntaxHighlighter {
 public:
 	SpellChecker(QObject* parent = nullptr);
 	~SpellChecker();
-		
+
 	// Common
 	static QString currentLanguage() {
 		QString overrideLocale = CoreManager::getInstance()->getSettingsModel()->getSpellCheckerOverrideLocale();
@@ -72,21 +72,21 @@ public:
 	Q_INVOKABLE void ignoreAll(QString word);
 	Q_INVOKABLE void replace(QString word, QString byWord, int cursorPosition);
 	Q_PROPERTY(QStringList redLines MEMBER redLines NOTIFY redLinesChanged);
-	
+
 	// Native (Mac/Windows) or ISpell
 	Q_INVOKABLE void learn(QString word);
 	Q_INVOKABLE QStringList suggestionsForWord(QString word);
 	bool isValid(QString word);
-	
+
 protected:
 	void highlightBlock(const QString &text) override;
-	
+
 public slots:
 	void highlightAfterGracePeriod();
-	
+
 signals:
 	void redLinesChanged();
-	
+
 private:
 	QTextCharFormat errorFormater;
 	QTimer *graceTimer;
@@ -111,24 +111,12 @@ private:
 #ifdef _WIN32
 	ISpellChecker* mNativeSpellChecker = nullptr;
 #endif
-	
-// ISpell linux
+
+// nuspell linux
 #ifdef __linux__
-	static int gISpell_sc_read_fd;
-	static int gISpell_sc_write_fd;
-	static int gISpell_app_read_fd;
-	static int gISpell_app_write_fd;
-	static std::thread *gISpellCheckerThread;
-	static QHash<QString,QStringList> gISpellSuggestions;
-	static std::string gISpellCheckeCurrentLanguage;
-	static std::string gIspellDictionariesFolder;
-	void stopISpellChecker();
-	static std::shared_ptr<linphone::Config> gISpellSelfDictionary;
-	bool isLearnt(QString word);
-	bool wordValidWithFrVariants(QString word);
-	bool validSplittedOn(QString pattern, QString word);
+	Sonnet::Speller dict;
 #endif
-	
+
 };
 
 
diff --git a/linphone-app/src/components/other/spell-checker/SpellCheckerLinux.cpp b/linphone-app/src/components/other/spell-checker/SpellCheckerLinux.cpp
index a9a56a48..4cb586dd 100644
--- a/linphone-app/src/components/other/spell-checker/SpellCheckerLinux.cpp
+++ b/linphone-app/src/components/other/spell-checker/SpellCheckerLinux.cpp
@@ -18,196 +18,34 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-
 #include "SpellChecker.hpp"
-#include <libispell.h>
-#include "app/paths/Paths.hpp"
-#include <unistd.h>
-#include <cstdio>
-#include <string>
+#include <Sonnet/Speller>
 #include <linphone++/linphone.hh>
-#include "utils/Utils.hpp"
-#include <QCryptographicHash>
-
-int SpellChecker::gISpell_sc_read_fd = 0;
-int SpellChecker::gISpell_sc_write_fd = 0;
-int SpellChecker::gISpell_app_read_fd = 0;
-int SpellChecker::gISpell_app_write_fd = 0;
-std::thread *SpellChecker::gISpellCheckerThread = nullptr;
-QHash<QString,QStringList> SpellChecker::gISpellSuggestions;
-std::string SpellChecker::gISpellCheckeCurrentLanguage;
-std::shared_ptr<linphone::Config> SpellChecker::gISpellSelfDictionary = linphone::Factory::get()->createConfig(Paths::getISpellOwnDictsDirPath());
-std::string SpellChecker::gIspellDictionariesFolder;
-
-
-bool open_channel(int& read_fd, int& write_fd) {
-	int vals[2];
-	int errc = pipe(vals);
-	if(errc) {
-		return false;
-	} else {
-		read_fd = vals[0];
-		write_fd = vals[1];
-		return true;
-	}
-}
-
-void SpellChecker::stopISpellChecker() {
-	QString stop("__spellchecker_stop__");
-	auto message = stop.toStdString();
-	ssize_t amnt_written = write(gISpell_sc_write_fd, message.data(), message.size());
-	if(amnt_written != message.size()) {
-		qWarning() << LOG_TAG << "Linux ispell unable to stop spell checker";
-	}
-	gISpellCheckerThread->join();
-	gISpellCheckerThread = nullptr;
-	mAvailable = false;
-}
 
 void SpellChecker::setLanguage() {
-	
-	QString locale = SpellChecker::currentLanguage().toLower().mid(0,2);
-	
-	if (gISpellCheckeCurrentLanguage == locale.toStdString() && gISpellCheckerThread != nullptr) {
-		mAvailable = true;
-		return;
-	}
-	
-	if (gISpellCheckerThread != nullptr) // Language change
-		stopISpellChecker();
-	
-	QString dict = Paths::getISpellDictsDirPath()+locale+".hash";
-	gIspellDictionariesFolder = Paths::getISpellDictsDirPath().toStdString();
-	
-	if (!QFile::exists(dict)) {
-		qWarning() << LOG_TAG << "Linux ispell language not supported " << SpellChecker::currentLanguage() << dict;
-		mAvailable = false;
-		return;
-	}
-	
-	if (!open_channel(gISpell_sc_read_fd, gISpell_sc_write_fd) ||
-		!open_channel(gISpell_app_read_fd, gISpell_app_write_fd)) {
-		qWarning() << LOG_TAG << "Linux ispell language unable to open channels";
-		mAvailable = false;
-		return;
-	}
-	
-	gISpellCheckeCurrentLanguage = locale.toStdString();
-	gISpellCheckerThread = new std::thread(bc_spell_checker,
-										   gIspellDictionariesFolder.data(),
-										   gISpellCheckeCurrentLanguage.data(),
-										   gISpell_sc_read_fd,
-										   gISpell_app_write_fd);
-	
-	mAvailable = true;
-	qDebug() << LOG_TAG << "Linux ispell language loaded from " << dict;
-}
-
-// Few special situation in French language not detected by the fr.hash.
-
-bool SpellChecker::wordValidWithFrVariants(QString word) {
-	if (word.toLower().contains("qu'")) {
-		QString replace = word.toLower().replace("qu'","que ");
-		if (isValid(replace)||validSplittedOn(" ",replace))
-			return true;
-	}
-	if (word.toLower().contains("s'")) {
-		QString replace = word.toLower().replace("s'","se ");
-		if (isValid(replace)||validSplittedOn(" ",replace))
-			return true;
-	}
-	return false;
-}
 
-bool SpellChecker::validSplittedOn(QString pattern, QString word) {
-	if (!word.contains(pattern))
-		return false;
-	auto split = word.split(pattern);
-	return isValid(split[0]) && isValid(split[1]);
+	dict = Sonnet::Speller(SpellChecker::currentLanguage());
+	mAvailable = dict.isValid();
+	qDebug() << LOG_TAG << "Linux ispell language loaded: " << SpellChecker::currentLanguage();
 }
 
 bool SpellChecker::isValid(QString word) {
-	
-	if (!mAvailable || word.length() == 1 || isLearnt(word))
-		return true;
-	
-	// no letters in word -> valid
-	QString wordCopy = word;
-	auto iterator = std::remove_if(wordCopy.begin(), wordCopy.end(), [](const QChar& c){ return !c.isLetter();});
-	wordCopy.chop(std::distance(iterator, wordCopy.end()));
-	if (wordCopy.isEmpty())
-		return true;
-	
-	// Some preformating
-	
-	word = word.replace("’","'");
-	word = word.replace("(","");
-	word = word.replace(")","");
-	word = word.replace("‘","'");
-	
-	while (word.endsWith(".") || word.endsWith("!") || word.endsWith(",") || word.endsWith(","))
-		word.chop(1);
-	
-	while (word.startsWith(".") || word.startsWith("!") || word.startsWith(",") || word.startsWith(",")) {
-		word = word.mid(1);
-	}
-		
-	// submit word to ispell
-	auto message = word.toStdString();
-	ssize_t amnt_written = write(gISpell_sc_write_fd, message.data(), message.size());
-	if(amnt_written != message.size()) {
-		qWarning() << LOG_TAG << "Linux ispell unable to communicate with spell checker thread";
+
+	if (!mAvailable || word.length() == 1)
 		return true;
-	}
-	
-	// wait and read ispell result
-	constexpr int buffer_size = 1024;
-	char buffer[buffer_size] = {0};
-	ssize_t amnt_read = read(gISpell_app_read_fd, &buffer[0], buffer_size);
-	QString returned = QString::fromUtf8(buffer);
-	if (returned == "1") {
+
+	if(dict.isCorrect(word)) {
 		return true;
-	} else {
-		if (!gISpellSuggestions.contains(word)) { // Record returned suggestions if any
-			QStringList returnedUggestions = returned.split(", ");
-			returnedUggestions.removeFirst();
-			gISpellSuggestions.insert(word,returnedUggestions);
-		}
-		return	(gISpellCheckeCurrentLanguage == "fr" && wordValidWithFrVariants(word)) ||
-			validSplittedOn("'",word) ||
-			validSplittedOn("-",word);
-	}
 
+	}
+	return false;
 }
 
 void SpellChecker::learn(QString word){
-	QCryptographicHash hash( QCryptographicHash::Sha1 ); // Hash to avoid fancy character conflict with config format.
-	hash.addData(word.toUtf8());
-	auto hashString = Utils::appStringToCoreString(hash.result().toHex());
-	gISpellSelfDictionary->setInt("words",hashString,1);
-	gISpellSelfDictionary->sync();
+	dict.addToPersonal(word);
 	highlightDocument();
 }
 
-bool SpellChecker::isLearnt(QString word){
-	QCryptographicHash hash( QCryptographicHash::Sha1 ); // Hash to avoid fancy character conflict with config format.
-	hash.addData(word.toUtf8());
-	auto hashString = Utils::appStringToCoreString(hash.result().toHex());
-	return gISpellSelfDictionary->getInt("words",hashString,0) == 1;
-}
-
 QStringList SpellChecker::suggestionsForWord(QString word) {
-	QStringList suggestions;
-	if (!gISpellSuggestions.contains(word))
-		return suggestions;
-	QListIterator<QString> itr (gISpellSuggestions.value(word));
-	while (itr.hasNext()) {
-		QString suggestion = itr.next();
-		if (!suggestion.contains("+"))
-			suggestions <<  suggestion;
-		if (suggestions.length() >= SUGGESTIONS_LIMIT) {
-			return suggestions;
-		}
-	}
-	return suggestions;
+	return dict.suggest(word);
 }
-- 
2.46.0

