diff --git a/projects/android-project/app/jni/Android.mk b/projects/android-project/app/jni/Android.mk index 55a1be419..3ed0ceb31 100644 --- a/projects/android-project/app/jni/Android.mk +++ b/projects/android-project/app/jni/Android.mk @@ -70,6 +70,7 @@ CORE_SRCS := tools/ecode/*.cpp \ tools/ecode/plugins/linter/*.cpp \ tools/ecode/plugins/formatter/*.cpp \ tools/ecode/plugins/lsp/*.cpp \ + tools/ecode/plugins/spellchecker/*.cpp \ tools/ecode/plugins/discordRPC/*.cpp \ tools/ecode/plugins/discordRPC/sdk/*.cpp \ tools/ecode/plugins/xmltools/*.cpp diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index b068207fa..a64ac0428 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -235,7 +235,7 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, "autocomplete" == creator.first || "linter" == creator.first || "autoformatter" == creator.first || "lspclient" == creator.first || "git" == creator.first || "debugger" == creator.first || - "aiassistant" == creator.first ); + "aiassistant" == creator.first || "spellchecker" == creator.first ); } languagesExtensions.priorities = ini.getKeyMap( "languages_extensions" ); diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index ed99f8400..a61a75682 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -25,6 +25,7 @@ #include "plugins/git/gitplugin.hpp" #include "plugins/linter/linterplugin.hpp" #include "plugins/lsp/lspclientplugin.hpp" +#include "plugins/spellchecker/spellcheckerplugin.hpp" #include "plugins/xmltools/xmltoolsplugin.hpp" #if EE_PLATFORM == EE_PLATFORM_LINUX @@ -609,6 +610,7 @@ void App::initPluginManager() { mPluginManager->registerPlugin( XMLToolsPlugin::Definition() ); mPluginManager->registerPlugin( GitPlugin::Definition() ); mPluginManager->registerPlugin( AIAssistantPlugin::Definition() ); + mPluginManager->registerPlugin( SpellCheckerPlugin::Definition() ); mPluginManager->registerPlugin( DiscordRPCplugin::Definition() ); } @@ -2643,6 +2645,7 @@ void App::onCodeEditorCreated( UICodeEditor* editor, TextDocument& doc ) { UITab* tab = (UITab*)editor->getData(); tab->removeClass( "tab_file_deleted" ); } + editor->getDocument().resetUndoRedo(); } ); editor->on( Event::OnCursorPosChangeInteresting, [this, editor]( auto ) { diff --git a/src/tools/ecode/plugins/linter/linterplugin.cpp b/src/tools/ecode/plugins/linter/linterplugin.cpp index 248059950..8503abdae 100644 --- a/src/tools/ecode/plugins/linter/linterplugin.cpp +++ b/src/tools/ecode/plugins/linter/linterplugin.cpp @@ -784,6 +784,7 @@ void LinterPlugin::lintDoc( std::shared_ptr doc ) { return; IOStreamString fileString; + mClock.restart(); if ( doc->isDirty() || !doc->hasFilepath() ) { std::string tmpPath; if ( !doc->hasFilepath() ) { @@ -814,7 +815,6 @@ void LinterPlugin::lintDoc( std::shared_ptr doc ) { void LinterPlugin::runLinter( std::shared_ptr doc, const Linter& linter, const std::string& path ) { - Clock clock; std::string cmd( linter.command ); std::string pathstr( "\"" + path + "\"" ); String::replaceAll( cmd, "$FILENAME", pathstr ); @@ -980,7 +980,7 @@ void LinterPlugin::runLinter( std::shared_ptr doc, const Linter& l Log::info( "LinterPlugin::runLinter with binary %s for %s took %.2fms. Found: %d matches. " "Errors: %d, " "Warnings: %d, Notices: %d.", - cmd, path, clock.getElapsedTime().asMilliseconds(), totalMatches, totalErrors, + cmd, path, mClock.getElapsedTime().asMilliseconds(), totalMatches, totalErrors, totalWarns, totalNotice ); } } diff --git a/src/tools/ecode/plugins/linter/linterplugin.hpp b/src/tools/ecode/plugins/linter/linterplugin.hpp index 8720dfe3a..b548c91a4 100644 --- a/src/tools/ecode/plugins/linter/linterplugin.hpp +++ b/src/tools/ecode/plugins/linter/linterplugin.hpp @@ -158,6 +158,7 @@ class LinterPlugin : public Plugin { std::string mErrorMsg; Rectf mQuickFixRect; std::string mOldMaxWidth; + Clock mClock; LinterPlugin( PluginManager* pluginManager, bool sync ); diff --git a/src/tools/ecode/plugins/lsp/lspclientplugin.cpp b/src/tools/ecode/plugins/lsp/lspclientplugin.cpp index 8855204d9..06252ef24 100644 --- a/src/tools/ecode/plugins/lsp/lspclientplugin.cpp +++ b/src/tools/ecode/plugins/lsp/lspclientplugin.cpp @@ -688,79 +688,6 @@ void LSPClientPlugin::setTrimLogs( bool trimLogs ) { mTrimLogs = trimLogs; } -bool LSPClientPlugin::editorExists( UICodeEditor* editor ) { - return mManager->getSplitter() && mManager->getSplitter()->editorExists( editor ); -} - -void LSPClientPlugin::createListView( UICodeEditor* editor, const std::shared_ptr& model, - const ModelEventCallback& onModelEventCb, - const std::function onCreateCb ) { - UICodeEditorSplitter* splitter = getManager()->getSplitter(); - if ( nullptr == splitter || !editorExists( editor ) ) - return; - editor->runOnMainThread( [model, editor, splitter, onModelEventCb, onCreateCb] { - auto lvs = editor->findAllByClass( "editor_listview" ); - for ( auto* ilv : lvs ) - ilv->close(); - - UIListView* lv = UIListView::New(); - lv->setParent( editor ); - lv->addClass( "editor_listview" ); - auto pos = - editor->getRelativeScreenPosition( editor->getDocumentRef()->getSelection().start() ); - lv->setPixelsPosition( { pos.x, pos.y + editor->getLineHeight() } ); - if ( !lv->getParent()->getLocalBounds().contains( - lv->getLocalBounds().setPosition( lv->getPixelsPosition() ) ) ) { - lv->setPixelsPosition( { pos.x, pos.y - lv->getPixelsSize().getHeight() } ); - } - lv->setVisible( true ); - lv->getVerticalScrollBar()->reloadStyle( true, true, true ); - lv->setAutoExpandOnSingleColumn( false ); - lv->setModel( model ); - Float height = std::min( lv->getContentSize().y, lv->getRowHeight() * 8 ); - Float colWidth = lv->getMaxColumnContentWidth( 0 ) + PixelDensity::dpToPx( 4 ); - bool needsVScroll = lv->getContentSize().y > lv->getRowHeight() * 8; - Float width = colWidth + lv->getPixelsPadding().getWidth() + - ( needsVScroll ? lv->getVerticalScrollBar()->getPixelsSize().getWidth() : 0 ); - lv->setPixelsSize( { width, height } ); - lv->setColumnWidth( 0, colWidth ); - lv->setScrollMode( needsVScroll ? ScrollBarMode::Auto : ScrollBarMode::AlwaysOff, - ScrollBarMode::AlwaysOff ); - if ( onCreateCb ) - onCreateCb( lv ); - lv->setSelection( model->index( 0 ) ); - lv->setFocus(); - Uint32 focusCb = lv->getUISceneNode()->getUIEventDispatcher()->addFocusEventCallback( - [lv]( const auto&, Node* focus, Node* ) { - if ( !lv->inParentTreeOf( focus ) && !lv->isClosing() ) - lv->close(); - } ); - Uint32 cursorCb = - editor->on( Event::OnCursorPosChange, [lv, editor, splitter]( const Event* ) { - if ( !lv->isClosing() ) { - lv->close(); - if ( splitter->editorExists( editor ) ) - editor->setFocus(); - } - } ); - lv->on( Event::KeyDown, [lv, splitter, editor]( const Event* event ) { - if ( event->asKeyEvent()->getKeyCode() == EE::Window::KEY_ESCAPE && !lv->isClosing() ) - lv->close(); - if ( splitter->editorExists( editor ) ) - editor->setFocus(); - } ); - lv->on( Event::OnModelEvent, [onModelEventCb]( const Event* event ) { - const ModelEvent* modelEvent = static_cast( event ); - if ( onModelEventCb ) - onModelEventCb( modelEvent ); - } ); - lv->on( Event::OnClose, [lv, editor, cursorCb, focusCb]( const Event* ) { - lv->getUISceneNode()->getUIEventDispatcher()->removeFocusEventCallback( focusCb ); - editor->removeEventListener( cursorCb ); - } ); - } ); -} - void LSPClientPlugin::createLocationsView( UICodeEditor* editor, const std::vector& res ) { auto model = LSPLocationModel::create( mManager->getWorkspaceFolder(), res ); diff --git a/src/tools/ecode/plugins/lsp/lspclientplugin.hpp b/src/tools/ecode/plugins/lsp/lspclientplugin.hpp index 11da92d6d..300e4a7ff 100644 --- a/src/tools/ecode/plugins/lsp/lspclientplugin.hpp +++ b/src/tools/ecode/plugins/lsp/lspclientplugin.hpp @@ -185,8 +185,6 @@ class LSPClientPlugin : public Plugin { void switchSourceHeader( UICodeEditor* editor ); - bool editorExists( UICodeEditor* editor ); - void createLocationsView( UICodeEditor* editor, const std::vector& locs ); void getAndGoToLocation( UICodeEditor* editor, const std::string& search ); @@ -195,12 +193,6 @@ class LSPClientPlugin : public Plugin { void createCodeActionsView( UICodeEditor* editor, const std::vector& cas ); - typedef std::function ModelEventCallback; - - void createListView( UICodeEditor* editor, const std::shared_ptr& model, - const ModelEventCallback& onModelEventCb, - const std::function onCreateCb = {} ); - PluginRequestHandle processTextDocumentSymbol( const PluginMessage& msg ); PluginRequestHandle processFoldingRanges( const PluginMessage& msg ); diff --git a/src/tools/ecode/plugins/plugin.cpp b/src/tools/ecode/plugins/plugin.cpp index 6b1265c8a..b762ca29e 100644 --- a/src/tools/ecode/plugins/plugin.cpp +++ b/src/tools/ecode/plugins/plugin.cpp @@ -1,6 +1,9 @@ #include "plugin.hpp" #include "pluginmanager.hpp" #include +#include +#include +#include namespace ecode { @@ -118,6 +121,79 @@ void Plugin::setReady( Time loadTime ) { } } +bool Plugin::editorExists( UICodeEditor* editor ) { + return mManager->getSplitter() && mManager->getSplitter()->editorExists( editor ); +} + +void Plugin::createListView( UICodeEditor* editor, std::shared_ptr model, + const ModelEventCallback& onModelEventCb, + const std::function onCreateCb ) { + UICodeEditorSplitter* splitter = getManager()->getSplitter(); + if ( nullptr == splitter || !editorExists( editor ) ) + return; + editor->runOnMainThread( [model, editor, splitter, onModelEventCb, onCreateCb] { + auto lvs = editor->findAllByClass( "editor_listview" ); + for ( auto* ilv : lvs ) + ilv->close(); + + UIListView* lv = UIListView::New(); + lv->setParent( editor ); + lv->addClass( "editor_listview" ); + auto pos = + editor->getRelativeScreenPosition( editor->getDocumentRef()->getSelection().start() ); + lv->setPixelsPosition( { pos.x, pos.y + editor->getLineHeight() } ); + if ( !lv->getParent()->getLocalBounds().contains( + lv->getLocalBounds().setPosition( lv->getPixelsPosition() ) ) ) { + lv->setPixelsPosition( { pos.x, pos.y - lv->getPixelsSize().getHeight() } ); + } + lv->setVisible( true ); + lv->getVerticalScrollBar()->reloadStyle( true, true, true ); + lv->setAutoExpandOnSingleColumn( false ); + lv->setModel( model ); + Float height = std::min( lv->getContentSize().y, lv->getRowHeight() * 8 ); + Float colWidth = lv->getMaxColumnContentWidth( 0 ) + PixelDensity::dpToPx( 4 ); + bool needsVScroll = lv->getContentSize().y > lv->getRowHeight() * 8; + Float width = colWidth + lv->getPixelsPadding().getWidth() + + ( needsVScroll ? lv->getVerticalScrollBar()->getPixelsSize().getWidth() : 0 ); + lv->setPixelsSize( { width, height } ); + lv->setColumnWidth( 0, colWidth ); + lv->setScrollMode( needsVScroll ? ScrollBarMode::Auto : ScrollBarMode::AlwaysOff, + ScrollBarMode::AlwaysOff ); + if ( onCreateCb ) + onCreateCb( lv ); + lv->setSelection( model->index( 0 ) ); + lv->setFocus(); + Uint32 focusCb = lv->getUISceneNode()->getUIEventDispatcher()->addFocusEventCallback( + [lv]( const auto&, Node* focus, Node* ) { + if ( !lv->inParentTreeOf( focus ) && !lv->isClosing() ) + lv->close(); + } ); + Uint32 cursorCb = + editor->on( Event::OnCursorPosChange, [lv, editor, splitter]( const Event* ) { + if ( !lv->isClosing() ) { + lv->close(); + if ( splitter->editorExists( editor ) ) + editor->setFocus(); + } + } ); + lv->on( Event::KeyDown, [lv, splitter, editor]( const Event* event ) { + if ( event->asKeyEvent()->getKeyCode() == EE::Window::KEY_ESCAPE && !lv->isClosing() ) + lv->close(); + if ( splitter->editorExists( editor ) ) + editor->setFocus(); + } ); + lv->on( Event::OnModelEvent, [onModelEventCb]( const Event* event ) { + const ModelEvent* modelEvent = static_cast( event ); + if ( onModelEventCb ) + onModelEventCb( modelEvent ); + } ); + lv->on( Event::OnClose, [lv, editor, cursorCb, focusCb]( const Event* ) { + lv->getUISceneNode()->getUIEventDispatcher()->removeFocusEventCallback( focusCb ); + editor->removeEventListener( cursorCb ); + } ); + } ); +} + PluginBase::~PluginBase() { mShuttingDown = true; unsubscribeFileSystemListener(); diff --git a/src/tools/ecode/plugins/plugin.hpp b/src/tools/ecode/plugins/plugin.hpp index a8d4e69b6..c3f5f5a61 100644 --- a/src/tools/ecode/plugins/plugin.hpp +++ b/src/tools/ecode/plugins/plugin.hpp @@ -13,6 +13,14 @@ namespace EE::System { class IniFile; } +namespace EE::UI { +class UIListView; +} + +namespace EE::UI::Abstract { +class ModelEvent; +} + namespace ecode { class PluginManager; @@ -79,6 +87,15 @@ class Plugin : public UICodeEditorPlugin { void setReady( Time loadTime = Seconds( 0 ) ); void waitUntilLoaded(); + + typedef std::function ModelEventCallback; + + bool editorExists( UICodeEditor* editor ); + + void createListView( UICodeEditor* editor, std::shared_ptr model, + const ModelEventCallback& onModelEventCb, + const std::function onCreateCb = {} ); + }; class PluginBase : public Plugin { @@ -128,7 +145,6 @@ class PluginBase : public Plugin { //! Usually used to unregister commands in a document virtual void onUnregisterDocument( TextDocument* ); - ; }; } // namespace ecode diff --git a/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.cpp b/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.cpp new file mode 100644 index 000000000..ce87215dd --- /dev/null +++ b/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.cpp @@ -0,0 +1,504 @@ +#include "spellcheckerplugin.hpp" +#include "eepp/ui/abstract/uiabstractview.hpp" +#include "eepp/ui/models/itemlistmodel.hpp" +#include +#include +#include +#include +#include +#include + +#include + +using json = nlohmann::json; + +namespace ecode { + +static constexpr std::string SPELL_CHECKER_CMD = "typos"; +static constexpr std::string SPELL_CHECKER_ARGS = "--format=brief"; + +Plugin* SpellCheckerPlugin::New( PluginManager* pluginManager ) { + return eeNew( SpellCheckerPlugin, ( pluginManager, false ) ); +} + +Plugin* SpellCheckerPlugin::NewSync( PluginManager* pluginManager ) { + return eeNew( SpellCheckerPlugin, ( pluginManager, true ) ); +} + +SpellCheckerPlugin::SpellCheckerPlugin( PluginManager* pluginManager, bool sync ) : + PluginBase( pluginManager ) { + if ( sync ) { + load( pluginManager ); + } else { + mThreadPool->run( [this, pluginManager] { load( pluginManager ); } ); + } +} + +SpellCheckerPlugin::~SpellCheckerPlugin() { + waitUntilLoaded(); + mShuttingDown = true; + + if ( mWorkersCount != 0 ) { + std::unique_lock lock( mWorkMutex ); + mWorkerCondition.wait( lock, [this]() { return mWorkersCount <= 0; } ); + } +} + +void SpellCheckerPlugin::load( PluginManager* pluginManager ) { + Clock clock; + AtomicBoolScopedOp loading( mLoading, true ); + std::string path = pluginManager->getPluginsPath() + "spellchecker.json"; + if ( FileSystem::fileExists( path ) || + FileSystem::fileWrite( path, "{\n \"config\":{},\n \"keybindings\":{}\n}\n" ) ) { + mConfigPath = path; + } + std::string data; + if ( !FileSystem::fileGet( path, data ) ) + return; + mConfigHash = String::hash( data ); + + json j; + try { + j = json::parse( data, nullptr, true, true ); + } catch ( const json::exception& e ) { + Log::error( + "SpellCheckerPlugin::load - Error parsing config from path %s, error: %s, config " + "file content:\n%s", + path.c_str(), e.what(), data.c_str() ); + // Recreate it + j = json::parse( "{\n \"config\":{},\n \"keybindings\":{},\n}\n", nullptr, true, true ); + } + + bool updateConfigFile = false; + + if ( j.contains( "config" ) ) { + auto& config = j["config"]; + if ( config.contains( "delay_time" ) ) + setDelayTime( Time::fromString( config["delay_time"].get() ) ); + else { + config["delay_time"] = getDelayTime().toString(); + updateConfigFile = true; + } + } + + if ( mKeyBindings.empty() ) { + mKeyBindings["spellchecker-fix-typo"] = "alt+return"; + } + + if ( j.contains( "keybindings" ) ) { + auto& kb = j["keybindings"]; + auto list = { "spellchecker-fix-typo" }; + for ( const auto& key : list ) { + if ( kb.contains( key ) ) { + if ( !kb[key].empty() ) + mKeyBindings[key] = kb[key]; + } else { + kb[key] = mKeyBindings[key]; + updateConfigFile = true; + } + } + } + + if ( updateConfigFile ) { + std::string newData = j.dump( 2 ); + if ( newData != data ) { + FileSystem::fileWrite( path, newData ); + mConfigHash = String::hash( newData ); + } + } + + subscribeFileSystemListener(); + mReady = true; + fireReadyCbs(); + setReady( clock.getElapsedTime() ); +} + +void SpellCheckerPlugin::onDocumentLoaded( TextDocument* doc ) { + setDocDirty( doc ); +} + +void SpellCheckerPlugin::onRegisterDocument( TextDocument* doc ) { + doc->setCommand( "spellchecker-fix-typo", [this]( TextDocument::Client* client ) { + createSpellCheckAlternativesView( static_cast( client ) ); + } ); + setDocDirty( doc ); +} + +void SpellCheckerPlugin::onUnregisterDocument( TextDocument* doc ) { + mDirtyDoc.erase( doc ); +} + +void SpellCheckerPlugin::onDocumentChanged( UICodeEditor*, TextDocument* oldDoc ) { + mDirtyDoc.erase( oldDoc ); +} + +void SpellCheckerPlugin::onRegisterListeners( UICodeEditor* editor, + std::vector& listeners ) { + listeners.push_back( editor->addEventListener( + Event::OnTextChanged, [this, editor]( const Event* ) { setDocDirty( editor ); } ) ); +} + +void SpellCheckerPlugin::setDocDirty( TextDocument* doc ) { + mDirtyDoc[doc] = std::make_unique(); +} + +void SpellCheckerPlugin::setDocDirty( UICodeEditor* editor ) { + mDirtyDoc[editor->getDocumentRef().get()] = std::make_unique(); +} + +void SpellCheckerPlugin::update( UICodeEditor* editor ) { + std::shared_ptr doc = editor->getDocumentRef(); + auto it = mDirtyDoc.find( doc.get() ); + if ( it != mDirtyDoc.end() && it->second->getElapsedTime() >= mDelayTime ) { + mDirtyDoc.erase( doc.get() ); + mThreadPool->run( [this, doc] { spellCheckDoc( doc ); } ); + } +} + +void SpellCheckerPlugin::spellCheckDoc( std::shared_ptr doc ) { + if ( !mLanguagesDisabled.empty() && + mLanguagesDisabled.find( doc->getSyntaxDefinition().getLSPName() ) != + mLanguagesDisabled.end() ) + return; + + ScopedOp op( + [this, doc]() { + std::lock_guard l( mWorkMutex ); + mWorkersCount++; + }, + [this]() { + { + std::lock_guard l( mWorkMutex ); + mWorkersCount--; + } + mWorkerCondition.notify_all(); + } ); + if ( !mReady ) + return; + + mTyposFound = !Sys::which( SPELL_CHECKER_CMD ).empty(); + + if ( !mTyposFound ) + return; + + IOStreamString fileString; + mClock.restart(); + if ( doc->isDirty() || !doc->hasFilepath() ) { + std::string tmpPath; + if ( !doc->hasFilepath() ) { + tmpPath = + Sys::getTempPath() + ".ecode-" + doc->getFilename() + "." + String::randString( 8 ); + } else { + tmpPath = Sys::getTempPath() + doc->getFilename(); + if ( FileSystem::fileExists( tmpPath ) ) { + tmpPath = Sys::getTempPath() + ".ecode-" + doc->getFilename() + "." + + String::randString( 8 ); + } + } + + doc->save( fileString, true ); + FileSystem::fileWrite( tmpPath, (Uint8*)fileString.getStreamPointer(), + fileString.getSize() ); + FileSystem::fileHide( tmpPath ); + runSpellChecker( doc, tmpPath ); + FileSystem::fileRemove( tmpPath ); + } else { + runSpellChecker( doc, doc->getFilePath() ); + } +} + +void SpellCheckerPlugin::runSpellChecker( std::shared_ptr doc, + const std::string& path ) { + std::string args( SPELL_CHECKER_ARGS + " \"" + path + "\"" ); + std::unique_ptr process = std::make_unique(); + TextDocument* docPtr = doc.get(); + ScopedOp op( + [this, &process, &docPtr] { + std::lock_guard l( mRunningProcessesMutex ); + auto found = mRunningProcesses.find( docPtr ); + if ( found != mRunningProcesses.end() ) { + found->second->kill(); + eeASSERT( found->second != process.get() ); + } + mRunningProcesses[docPtr] = process.get(); + }, + [this, &docPtr] { + std::lock_guard l( mRunningProcessesMutex ); + mRunningProcesses.erase( docPtr ); + } ); + + if ( process->create( SPELL_CHECKER_CMD, args, + Process::getDefaultOptions() | Process::CombinedStdoutStderr, {}, + mManager->getWorkspaceFolder() ) ) { + int returnCode = 0; + std::string data; + process->readAllStdOut( data, Seconds( 30 ) ); + + if ( mShuttingDown ) { + process->kill(); + return; + } + + if ( process->isAlive() ) { + process->join( &returnCode ); + process->destroy(); + } else if ( process->killed() ) { + return; + } + + if ( 0 != returnCode ) { + Lock matchesLock( mMatchesMutex ); + std::map> empty; + setMatches( doc.get(), std::move( empty ) ); + return; + } + + std::map> matches; + size_t totalMatches = 0; + + String::readBySeparator( data, [doc, &matches, &totalMatches]( std::string_view chunk ) { + LuaPattern pattern( "[^:]:(%d+):(%d+):%s`(.-)`%s?%-%>%s?([^\n]*)" ); + std::string data{ chunk }; + for ( auto& match : pattern.gmatch( data ) ) { + SpellCheckerMatch spellCheckerMatch; + std::string lineStr = match.group( 1 ); + std::string colStr = match.group( 2 ); + spellCheckerMatch.text = match.group( 3 ); + String::trimInPlace( spellCheckerMatch.text ); + String::trimInPlace( spellCheckerMatch.text, '\n' ); + Int64 line; + Int64 col = 1; + if ( !spellCheckerMatch.text.empty() && !lineStr.empty() && + String::fromString( line, lineStr ) ) { + if ( !colStr.empty() ) + String::fromString( col, colStr ); + spellCheckerMatch.range.setStart( + { line > 0 ? line - 1 : 0, col > 0 ? col - 1 : 0 } ); + spellCheckerMatch.range.setEnd( + { spellCheckerMatch.range.start().line(), + spellCheckerMatch.range.start().column() + + (Int64)String( spellCheckerMatch.text ).size() } ); + spellCheckerMatch.range = spellCheckerMatch.range.normalized(); + spellCheckerMatch.range = doc->sanitizeRange( spellCheckerMatch.range ); + spellCheckerMatch.lineHash = + doc->getLineHash( spellCheckerMatch.range.start().line() ); + + std::vector alternatives = String::split( match.group( 4 ), ',' ); + if ( !alternatives.empty() ) { + for ( auto& alt : alternatives ) { + String::trimInPlace( alt, ' ' ); + String::trimInPlace( alt, '`' ); + } + spellCheckerMatch.alternatives = std::move( alternatives ); + matches[line - 1].emplace_back( std::move( spellCheckerMatch ) ); + totalMatches++; + } + } + } + } ); + + // Log::debug( "SpellChecker result:\n%s", data.c_str() ); + + setMatches( doc.get(), std::move( matches ) ); + + Log::info( "SpellCheckerPlugin::runSpellChecker with binary %s for %s took %.2fms. Found: " + "%d matches.", + SPELL_CHECKER_CMD, path, mClock.getElapsedTime().asMilliseconds(), + totalMatches ); + } +} + +void SpellCheckerPlugin::invalidateEditors( TextDocument* doc ) { + Lock l( mMutex ); + for ( auto& it : mEditorDocs ) { + if ( it.second == doc ) + it.first->invalidateDraw(); + } +} + +void SpellCheckerPlugin::setMatches( TextDocument* doc, + std::map>&& matches ) { + Lock matchesLock( mMatchesMutex ); + mMatches[doc] = std::move( matches ); + invalidateEditors( doc ); +} + +void SpellCheckerPlugin::drawAfterLineText( UICodeEditor* editor, const Int64& index, + Vector2f position, const Float& /*fontSize*/, + const Float& lineHeight ) { + Lock l( mMatchesMutex ); + auto matchIt = mMatches.find( editor->getDocumentRef().get() ); + if ( matchIt == mMatches.end() ) + return; + + std::map>& map = matchIt->second; + auto lineIt = map.find( index ); + if ( lineIt == map.end() ) + return; + TextDocument* doc = matchIt->first; + std::vector& matches = lineIt->second; + + for ( size_t i = 0; i < matches.size(); ++i ) { + auto& match = matches[i]; + + if ( match.lineHash != doc->getLineHash( index ) ) + return; + + Text line( "", editor->getFont(), editor->getFontSize() ); + Color color( editor->getColorScheme().getEditorSyntaxStyle( "notice"_sst ).color ); + line.setTabWidth( editor->getTabWidth() ); + line.setStyleConfig( editor->getFontStyleConfig() ); + line.setColor( color ); + + auto rects = editor->getTextRangeRectangles( { match.range }, editor->getScreenScroll(), {}, + lineHeight ); + if ( rects.empty() ) + continue; + + Int64 strSize = match.range.end().column() - match.range.start().column(); + Vector2f pos = rects[0].getPosition(); + + if ( strSize <= 0 ) { + strSize = 1; + pos = { position.x, position.y }; + } + + std::string str( strSize, '~' ); + String string( str ); + line.setString( string ); + Rectf box( pos - editor->getScreenPos(), { editor->getTextWidth( string ), lineHeight } ); + match.box[editor] = box; + line.draw( pos.x, pos.y + lineHeight * 0.5f ); + } +} + +void SpellCheckerPlugin::minimapDrawBefore( + UICodeEditor* editor, const DocumentLineRange& docLineRange, const DocumentViewLineRange&, + const Vector2f& /*linePos*/, const Vector2f& /*lineSize*/, const Float& /*charWidth*/, + const Float& /*gutterWidth*/, const DrawTextRangesFn& drawTextRanges ) { + Lock l( mMatchesMutex ); + auto matchIt = mMatches.find( editor->getDocumentRef().get() ); + if ( matchIt == mMatches.end() ) + return; + + TextDocument* doc = matchIt->first; + for ( const auto& matches : matchIt->second ) { + for ( const auto& match : matches.second ) { + if ( match.range.intersectsLineRange( docLineRange ) ) { + if ( match.lineHash != doc->getLineHash( match.range.start().line() ) ) + return; + Color col( editor->getColorScheme().getEditorSyntaxStyle( "notice"_sst ).color ); + col.blendAlpha( 100 ); + drawTextRanges( match.range, col, true ); + } + } + } +} + +std::optional +SpellCheckerPlugin::getMatchFromTextPosition( UICodeEditor* editor, TextPosition textPos ) { + + Mutex mMatchesMutex; + auto docMatch = mMatches.find( &editor->getDocument() ); + if ( docMatch == mMatches.end() ) + return {}; + auto lineMatch = docMatch->second.find( textPos.line() ); + if ( lineMatch == docMatch->second.end() ) + return {}; + const auto& spellCheckerMatches = lineMatch->second; + auto checkerMatch = std::find_if( + spellCheckerMatches.begin(), spellCheckerMatches.end(), + [&textPos]( const SpellCheckerMatch& match ) { return match.range.contains( textPos ); } ); + if ( checkerMatch == spellCheckerMatches.end() ) + return {}; + return *checkerMatch; +} + +std::optional SpellCheckerPlugin::getMatchFromScreenPos( UICodeEditor* editor, + Vector2f pos ) { + return getMatchFromTextPosition( editor, editor->resolveScreenPosition( pos ) ); +} + +void SpellCheckerPlugin::replaceMatchWithText( const TextRange& range, const std::string& newText, + UICodeEditor* editor ) { + { + // If dirty we must cancel for safety + Lock l( mMutex ); + if ( mDirtyDoc.find( &editor->getDocument() ) != mDirtyDoc.end() ) + return; + } + + auto doc = editor->getDocumentRef(); + ; + auto sels = doc->getSelections(); + + doc->resetSelection( { range } ); + doc->textInput( newText ); + + doc->resetSelection( sels ); +} + +bool SpellCheckerPlugin::onCreateContextMenu( UICodeEditor* editor, UIPopUpMenu* menu, + const Vector2i& position, const Uint32& /*flags*/ ) { + if ( !mTyposFound ) + return false; + + auto pickedMatch = getMatchFromScreenPos( editor, position.asFloat() ); + if ( !pickedMatch ) + return false; + + menu->addSeparator(); + + auto range = pickedMatch->range; + auto* subMenu = UIPopUpMenu::New(); + subMenu->addClass( "spellchecker_menu" ); + subMenu->on( Event::OnItemClicked, [range, this, editor]( const Event* event ) { + UIMenuItem* item = event->getNode()->asType(); + std::string id( item->getId() ); + replaceMatchWithText( range, id, editor ); + } ); + + auto addFn = [this, subMenu]( const std::string& txtKey, const std::string& txtVal, + const std::string& icon = "" ) { + subMenu + ->add( i18n( txtKey, txtVal ), + !icon.empty() ? findIcon( icon )->getSize( PixelDensity::dpToPxI( 12 ) ) + : nullptr, + KeyBindings::keybindFormat( mKeyBindings[txtKey] ) ) + ->setId( txtKey ); + }; + + for ( const auto& alt : pickedMatch->alternatives ) + addFn( alt, alt ); + + menu->addSubMenu( i18n( "spell-checker", "Spell Checker" ), nullptr, subMenu ); + + return false; +} + +void SpellCheckerPlugin::createSpellCheckAlternativesView( UICodeEditor* editor ) { + auto match = getMatchFromTextPosition( editor, editor->getDocument().getSelection().start() ); + if ( !match || match->alternatives.empty() ) + return; + + auto range = match->range; + createListView( editor, ItemListOwnerModel::create( match->alternatives ), + [this, range, editor]( const ModelEvent* modelEvent ) { + if ( modelEvent->getModelEventType() != ModelEventType::Open ) + return; + auto str = + modelEvent->getModel()->data( modelEvent->getModelIndex() ).toString(); + replaceMatchWithText( range, str, editor ); + modelEvent->getNode()->close(); + } ); +} + +const Time& SpellCheckerPlugin::getDelayTime() const { + return mDelayTime; +} + +void SpellCheckerPlugin::setDelayTime( const Time& delayTime ) { + mDelayTime = delayTime; +} + +} // namespace ecode diff --git a/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.hpp b/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.hpp new file mode 100644 index 000000000..7ab5bdab4 --- /dev/null +++ b/src/tools/ecode/plugins/spellchecker/spellcheckerplugin.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include "../plugin.hpp" +#include "../pluginmanager.hpp" +#include + +#include + +namespace ecode { + +struct SpellCheckerMatch { + std::string text; + TextRange range; + String::HashType lineHash{ 0 }; + std::unordered_map box; + std::vector alternatives; +}; + +class SpellCheckerPlugin : public PluginBase { + public: + static PluginDefinition Definition() { + return { "spellchecker", + "Spell Checker", + "Check the spelling of the current documents.", + SpellCheckerPlugin::New, + { 0, 0, 1 }, + SpellCheckerPlugin::NewSync }; + } + + static Plugin* New( PluginManager* pluginManager ); + + static Plugin* NewSync( PluginManager* pluginManager ); + + virtual ~SpellCheckerPlugin(); + + std::string getId() override { return Definition().id; } + + std::string getTitle() override { return Definition().name; } + + std::string getDescription() override { return Definition().description; } + + void drawAfterLineText( UICodeEditor* editor, const Int64& index, Vector2f position, + const Float& fontSize, const Float& lineHeight ) override; + + void minimapDrawBefore( UICodeEditor* /*editor*/, const DocumentLineRange&, + const DocumentViewLineRange&, const Vector2f& /*linePos*/, + const Vector2f& /*lineSize*/, const Float& /*charWidth*/, + const Float& /*gutterWidth*/, + const DrawTextRangesFn& /* drawTextRanges */ ) override; + + bool onCreateContextMenu( UICodeEditor* editor, UIPopUpMenu* menu, const Vector2i& position, + const Uint32& flags ) override; + + const Time& getDelayTime() const; + + void setDelayTime( const Time& delayTime ); + + protected: + Time mDelayTime{ Seconds( 0.5f ) }; + std::unordered_map> mDirtyDoc; + std::unordered_map>> mMatches; + std::set mLanguagesDisabled; + std::set mLSPLanguagesDisabled; + std::mutex mWorkMutex; + std::condition_variable mWorkerCondition; + Int32 mWorkersCount{ 0 }; + std::mutex mRunningProcessesMutex; + std::unordered_map mRunningProcesses; + Mutex mMatchesMutex; + bool mTyposFound{ false }; + Clock mClock; + + SpellCheckerPlugin( PluginManager* pluginManager, bool sync ); + + void load( PluginManager* pluginManager ); + + void onDocumentLoaded( TextDocument* ) override; + + void onRegisterDocument( TextDocument* ) override; + + void onUnregisterDocument( TextDocument* ) override; + + void onDocumentChanged( UICodeEditor*, TextDocument* /*oldDoc*/ ) override; + + void onRegisterListeners( UICodeEditor*, std::vector& /*listeners*/ ) override; + + void update( UICodeEditor* ) override; + + void setDocDirty( TextDocument* doc ); + + void setDocDirty( UICodeEditor* editor ); + + void spellCheckDoc( std::shared_ptr doc ); + + void runSpellChecker( std::shared_ptr doc, const std::string& path ); + + void setMatches( TextDocument* doc, std::map>&& matches ); + + void invalidateEditors( TextDocument* doc ); + + std::optional getMatchFromTextPosition( UICodeEditor* editor, TextPosition pos ); + + std::optional getMatchFromScreenPos( UICodeEditor* editor, Vector2f pos ); + + void replaceMatchWithText( const TextRange& range, const std::string& newText, UICodeEditor* ); + + void createSpellCheckAlternativesView( UICodeEditor* editor ); +}; + +} // namespace ecode