diff --git a/include/eepp/system/scopedop.hpp b/include/eepp/system/scopedop.hpp index 7298df868..f5ef01992 100644 --- a/include/eepp/system/scopedop.hpp +++ b/include/eepp/system/scopedop.hpp @@ -36,6 +36,26 @@ class BoolScopedOp { bool& boolRef; }; +class BoolScopedOpOptional { + public: + explicit BoolScopedOpOptional( bool cond, bool& boolRef ) : cond( cond ), boolRef( boolRef ) {} + + explicit BoolScopedOpOptional( bool cond, bool& boolRef, bool initialVal ) : + cond( cond ), boolRef( boolRef ) { + if ( cond ) + boolRef = initialVal; + } + + ~BoolScopedOpOptional() { + if ( cond ) + boolRef = !boolRef; + } + + private: + bool cond; + bool& boolRef; +}; + }} // namespace EE::System #endif // EE_SYSTEM_SCOPEDOP_HPP diff --git a/include/eepp/ui/doc/textdocument.hpp b/include/eepp/ui/doc/textdocument.hpp index 226d9d24a..ed19bf7f4 100644 --- a/include/eepp/ui/doc/textdocument.hpp +++ b/include/eepp/ui/doc/textdocument.hpp @@ -556,13 +556,13 @@ class EE_API TextDocument { SyntaxHighlighter* getHighlighter() const; - TextRange getWordRangeInPosition( const TextPosition& pos ); + TextRange getWordRangeInPosition( const TextPosition& pos, bool basedOnHighlighter = true ); - TextRange getWordRangeInPosition(); + TextRange getWordRangeInPosition( bool basedOnHighlighter = true ); - String getWordInPosition( const TextPosition& pos ); + String getWordInPosition( const TextPosition& pos, bool basedOnHighlighter = true ); - String getWordInPosition(); + String getWordInPosition( bool basedOnHighlighter = true ); bool mightBeBinary() const; @@ -578,6 +578,10 @@ class EE_API TextDocument { void stopActiveFindAll(); + bool isDoingTextInput() const; + + bool isInsertingText() const; + protected: friend class UndoStack; @@ -609,6 +613,7 @@ class EE_API TextDocument { bool mHAsCpp{ false }; bool mLastCursorChangeWasInteresting{ false }; bool mDoingTextInput{ false }; + bool mInsertingText{ false }; std::vector> mAutoCloseBracketsPairs; Uint32 mIndentWidth{ 4 }; IndentType mIndentType{ IndentType::IndentTabs }; diff --git a/include/eepp/ui/doc/textrange.hpp b/include/eepp/ui/doc/textrange.hpp index 67996ae03..70af8318a 100644 --- a/include/eepp/ui/doc/textrange.hpp +++ b/include/eepp/ui/doc/textrange.hpp @@ -71,6 +71,22 @@ class EE_API TextRange { return mStart >= other.mStart && mEnd >= other.mEnd; } + TextRange operator+( const TextRange& other ) const { + return TextRange( mStart + other.mStart, mEnd + other.mEnd ); + } + + TextRange operator+=( const TextRange& other ) const { + return TextRange( mStart + other.mStart, mEnd + other.mEnd ); + } + + TextRange operator-( const TextRange& other ) const { + return TextRange( mStart - other.mStart, mEnd - other.mEnd ); + } + + TextRange operator-=( const TextRange& other ) const { + return TextRange( mStart - other.mStart, mEnd - other.mEnd ); + } + bool contains( const TextPosition& position ) const { if ( !( position.line() > mStart.line() || ( position.line() == mStart.line() && position.column() >= mStart.column() ) ) ) diff --git a/projects/linux/ee.files b/projects/linux/ee.files index 93290f423..eff466703 100644 --- a/projects/linux/ee.files +++ b/projects/linux/ee.files @@ -1238,6 +1238,8 @@ ../../src/tools/ecode/plugins/lsp/lspprotocol.hpp ../../src/tools/ecode/plugins/pluginmanager.cpp ../../src/tools/ecode/plugins/pluginmanager.hpp +../../src/tools/ecode/plugins/xmltools/xmltoolsplugin.cpp +../../src/tools/ecode/plugins/xmltools/xmltoolsplugin.hpp ../../src/tools/ecode/projectbuild.cpp ../../src/tools/ecode/projectbuild.hpp ../../src/tools/ecode/projectdirectorytree.cpp diff --git a/src/eepp/ui/doc/textdocument.cpp b/src/eepp/ui/doc/textdocument.cpp index cd7e166d9..7dfe5262a 100644 --- a/src/eepp/ui/doc/textdocument.cpp +++ b/src/eepp/ui/doc/textdocument.cpp @@ -1266,7 +1266,7 @@ TextRange TextDocument::getDocRange() const { void TextDocument::deleteTo( const size_t& cursorIdx, int offset ) { eeASSERT( cursorIdx < mSelection.size() ); - BoolScopedOp op( mDoingTextInput, true ); + BoolScopedOpOptional op( !mDoingTextInput, mDoingTextInput, true ); TextPosition cursorPos = mSelection[cursorIdx].normalized().start(); if ( mSelection[cursorIdx].hasSelection() ) { remove( cursorIdx, getSelectionIndex( cursorIdx ) ); @@ -1281,7 +1281,7 @@ void TextDocument::deleteTo( const size_t& cursorIdx, int offset ) { } void TextDocument::deleteSelection( const size_t& cursorIdx ) { - BoolScopedOp op( mDoingTextInput, true ); + BoolScopedOpOptional op( !mDoingTextInput, mDoingTextInput, true ); TextPosition cursorPos = getSelectionIndex( cursorIdx ).normalized().start(); remove( cursorIdx, getSelectionIndex( cursorIdx ) ); setSelection( cursorIdx, cursorPos ); @@ -1377,6 +1377,7 @@ std::vector TextDocument::autoCloseBrackets( const String& text ) { void TextDocument::textInput( const String& text, bool mightBeInteresting ) { BoolScopedOp op( mDoingTextInput, true ); + BoolScopedOp op2( mInsertingText, true ); if ( mAutoCloseBrackets && 1 == text.size() ) { auto inserted = autoCloseBrackets( text ); @@ -1574,7 +1575,7 @@ void TextDocument::deleteToNextWord() { } void TextDocument::deleteCurrentLine() { - BoolScopedOp op( mDoingTextInput, true ); + BoolScopedOpOptional op( !mDoingTextInput, mDoingTextInput, true ); for ( size_t i = 0; i < mSelection.size(); ++i ) { if ( mSelection[i].hasSelection() ) { deleteSelection( i ); @@ -1615,8 +1616,8 @@ void TextDocument::selectToNextWord() { mergeSelection(); } -TextRange TextDocument::getWordRangeInPosition( const TextPosition& pos ) { - if ( mHighlighter ) { +TextRange TextDocument::getWordRangeInPosition( const TextPosition& pos, bool basedOnHighlighter ) { + if ( mHighlighter && basedOnHighlighter ) { auto type( mHighlighter->getTokenPositionAt( pos ) ); return { { pos.line(), type.pos }, { pos.line(), type.pos + (Int64)type.len } }; } @@ -1624,16 +1625,16 @@ TextRange TextDocument::getWordRangeInPosition( const TextPosition& pos ) { return { nextWordBoundary( pos, false ), previousWordBoundary( pos, false ) }; } -TextRange TextDocument::getWordRangeInPosition() { - return getWordRangeInPosition( getSelection().start() ); +TextRange TextDocument::getWordRangeInPosition( bool basedOnHighlighter ) { + return getWordRangeInPosition( getSelection().start(), basedOnHighlighter ); } -String TextDocument::getWordInPosition( const TextPosition& pos ) { - return getText( getWordRangeInPosition( pos ) ); +String TextDocument::getWordInPosition( const TextPosition& pos, bool basedOnHighlighter ) { + return getText( getWordRangeInPosition( pos, basedOnHighlighter ) ); } -String TextDocument::getWordInPosition() { - return getWordInPosition( getSelection().start() ); +String TextDocument::getWordInPosition( bool basedOnHighlighter ) { + return getWordInPosition( getSelection().start(), basedOnHighlighter ); } bool TextDocument::mightBeBinary() const { @@ -1771,7 +1772,7 @@ void TextDocument::newLineAbove() { } void TextDocument::insertAtStartOfSelectedLines( const String& text, bool skipEmpty ) { - BoolScopedOp op( mDoingTextInput, true ); + BoolScopedOpOptional op( !mDoingTextInput, mDoingTextInput, true ); TextPosition prevStart = getSelection().start(); TextRange range = getSelection( true ); bool swap = prevStart != range.start(); @@ -1787,7 +1788,7 @@ void TextDocument::insertAtStartOfSelectedLines( const String& text, bool skipEm void TextDocument::removeFromStartOfSelectedLines( const String& text, bool skipEmpty, bool removeExtraSpaces ) { - BoolScopedOp op( mDoingTextInput, true ); + BoolScopedOpOptional op( !mDoingTextInput, mDoingTextInput, true ); TextPosition prevStart = getSelection().start(); TextRange range = getSelection( true ); bool swap = prevStart != range.start(); @@ -1904,7 +1905,7 @@ void TextDocument::setIndentWidth( const Uint32& tabWidth ) { } void TextDocument::deleteTo( const size_t& cursorIdx, TextPosition position ) { - BoolScopedOp op( mDoingTextInput, true ); + BoolScopedOpOptional op( !mDoingTextInput, mDoingTextInput, true ); TextPosition cursorPos = getSelectionIndex( cursorIdx ).normalized().start(); if ( getSelectionIndex( cursorIdx ).hasSelection() ) { remove( cursorIdx, getSelectionIndex( cursorIdx ) ); @@ -2384,6 +2385,14 @@ void TextDocument::stopActiveFindAll() { *stopFlag.second.get() = true; } +bool TextDocument::isDoingTextInput() const { + return mDoingTextInput; +} + +bool TextDocument::isInsertingText() const { + return mInsertingText; +} + TextRanges TextDocument::findAll( const String& text, bool caseSensitive, bool wholeWord, const FindReplaceType& type, TextRange restrictRange, size_t maxResults ) { @@ -2543,120 +2552,97 @@ TextPosition TextDocument::getMatchingBracket( TextPosition sp, return {}; } -TextRange TextDocument::getMatchingBracket( TextPosition sp, const String& openBracket, +TextRange TextDocument::getMatchingBracket( TextPosition start, const String& openBracket, const String& closeBracket, MatchDirection dir ) { - if ( !sp.isValid() ) + if ( !start.isValid() ) return {}; SyntaxHighlighter* highlighter = getHighlighter(); if ( dir == MatchDirection::Forward ) { { - TextPosition end( positionOffset( sp, openBracket.size() ) ); + TextPosition end( positionOffset( start, openBracket.size() ) ); // Skip the open string if the start position is from there. Always start with depth 1 if ( end.isValid() ) { - String text = getText( { sp, end } ); + String text = getText( { start, end } ); if ( text == openBracket ) - sp = end; + start = end; } } // Ensure there's a close bracket - auto foundClose = find( closeBracket, sp ); + auto foundClose = find( closeBracket, start ); if ( !foundClose.isValid() ) return {}; // Not found, exit - TextRange foundOpen = { sp, sp }; + TextRange foundOpen = { start, start }; int depth = 1; do { - foundOpen = find( openBracket, foundOpen.end(), true, false, - TextDocument::FindReplaceType::Normal, - { foundOpen.end(), foundClose.start() } ); - if ( foundOpen.isValid() ) - changeDepth( highlighter, depth, foundOpen.start(), 1 ); - } while ( foundOpen.isValid() ); - - // Didn't fint more open brackets, the depth is the same, we found the close correct bracket - if ( depth == 1 ) - return foundClose; - - // Start balanced search from the first close bracket found - sp = foundClose.end(); - do { - auto findOpen = find( openBracket, sp ); - if ( findOpen.isValid() ) { - changeDepth( highlighter, depth, findOpen.start(), 1 ); - sp = findOpen.end(); - foundClose = find( closeBracket, sp ); - if ( foundClose.isValid() ) { - changeDepth( highlighter, depth, foundClose.start(), -1 ); + // Find all the open brackets between the first open bracket and the first close bracket + do { + foundOpen = + find( openBracket, start, true, false, TextDocument::FindReplaceType::Normal, + { start, foundClose.start() } ); + if ( foundOpen.isValid() ) { + start = foundOpen.end(); + changeDepth( highlighter, depth, start, 1 ); } else { - break; // Unexpected, fail - } - } else { - foundClose = find( closeBracket, sp ); - if ( foundClose.isValid() ) { - changeDepth( highlighter, depth, foundClose.start(), -1 ); - sp = foundClose.end(); - } else { - break; // Unexpected, fail + start = foundClose.end(); + changeDepth( highlighter, depth, start, -1 ); } + } while ( foundOpen.isValid() ); + + if ( depth > 0 ) { + // Find the next close bracket from the last close bracket + foundClose = find( closeBracket, start ); + if ( !foundClose.isValid() ) + break; } } while ( depth > 0 ); + return foundClose; } else { { - TextPosition end( positionOffset( sp, -closeBracket.size() ) ); - // Skip the cloes string if the start position is from there. Always start with depth 1 + TextPosition end( positionOffset( start, -closeBracket.size() ) ); + // Skip the close string if the start position is from there. Always start with depth 1 if ( end.isValid() ) { - String text = getText( { end, sp } ); + String text = getText( { end, start } ); if ( text == closeBracket ) - sp = end; + start = end; } } // Ensure there's an open bracket - auto foundOpen = findLast( openBracket, sp ); + auto foundOpen = findLast( openBracket, start ); if ( !foundOpen.isValid() ) return {}; // Not found, exit - TextRange foundClose = { sp, sp }; + TextRange foundClose = { start, start }; int depth = 1; do { - foundClose = findLast( closeBracket, foundClose.end(), true, false, - TextDocument::FindReplaceType::Normal, - { foundClose.end(), foundOpen.start() } ); - if ( foundClose.isValid() ) - changeDepth( highlighter, depth, foundClose.start(), 1 ); - } while ( foundClose.isValid() ); - - // Didn't fint more open brackets, the depth is the same, we found the close correct bracket - if ( depth == 1 ) - return foundOpen; - - // Start balanced search from the first open bracket found - sp = foundOpen.end(); - do { - auto findClose = findLast( closeBracket, sp ); - if ( findClose.isValid() ) { - changeDepth( highlighter, depth, findClose.start(), 1 ); - sp = findClose.end(); - foundOpen = findLast( openBracket, sp ); - if ( foundOpen.isValid() ) { - changeDepth( highlighter, depth, foundOpen.start(), -1 ); + // Find all the close brackets between the first close bracket and the first open + // bracket + do { + foundClose = + findLast( closeBracket, start, true, false, + TextDocument::FindReplaceType::Normal, { start, foundOpen.start() } ); + if ( foundClose.isValid() ) { + start = foundClose.end(); + changeDepth( highlighter, depth, start, 1 ); } else { - break; // Unexpected, fail - } - } else { - foundOpen = findLast( openBracket, sp ); - if ( foundOpen.isValid() ) { - changeDepth( highlighter, depth, foundOpen.start(), -1 ); - sp = foundOpen.end(); - } else { - break; // Unexpected, fail + start = foundOpen.end(); + changeDepth( highlighter, depth, start, -1 ); } + } while ( foundClose.isValid() ); + + if ( depth > 0 ) { + // Find the next open bracket from the last open bracket + foundOpen = findLast( openBracket, start ); + if ( !foundOpen.isValid() ) + break; } } while ( depth > 0 ); + return foundOpen; } } diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 4cc7f0872..e66f936d4 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -6,6 +6,7 @@ #include "plugins/formatter/formatterplugin.hpp" #include "plugins/linter/linterplugin.hpp" #include "plugins/lsp/lspclientplugin.hpp" +#include "plugins/xmltools/xmltoolsplugin.hpp" #include "settingsmenu.hpp" #include "uibuildsettings.hpp" #include "uiwelcomescreen.hpp" @@ -429,6 +430,7 @@ void App::initPluginManager() { mPluginManager->registerPlugin( FormatterPlugin::Definition() ); mPluginManager->registerPlugin( AutoCompletePlugin::Definition() ); mPluginManager->registerPlugin( LSPClientPlugin::Definition() ); + mPluginManager->registerPlugin( XMLToolsPlugin::Definition() ); } void App::loadConfig( const LogLevel& logLevel, const Sizeu& displaySize, bool sync, @@ -3508,8 +3510,7 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { std::vector adedLangs; if ( SyntaxDefinitionManager::instance()->loadFromStream( sfile, &adedLangs ) ) { for ( const auto& lang : adedLangs ) { - const auto& def = - SyntaxDefinitionManager::instance()->getByLanguageName( lang ); + const auto& def = SyntaxDefinitionManager::instance()->getByLanguageName( lang ); auto code = SyntaxDefinitionManager::toCPP( def ); if ( convertLangOutput && !convertLangOutput.Get().empty() && FileSystem::isDirectory( convertLangOutput.Get() ) ) { diff --git a/src/tools/ecode/plugins/formatter/formatterplugin.cpp b/src/tools/ecode/plugins/formatter/formatterplugin.cpp index 67bbe7f80..6ffec2e45 100644 --- a/src/tools/ecode/plugins/formatter/formatterplugin.cpp +++ b/src/tools/ecode/plugins/formatter/formatterplugin.cpp @@ -43,9 +43,6 @@ FormatterPlugin::FormatterPlugin( PluginManager* pluginManager, bool sync ) : load( pluginManager ); #endif } - mManager->subscribeMessages( this, [this]( const PluginMessage& msg ) -> PluginRequestHandle { - return processMessage( msg ); - } ); } FormatterPlugin::~FormatterPlugin() { @@ -187,7 +184,9 @@ void FormatterPlugin::loadFormatterConfig( const std::string& path, bool updateC j["keybindings"]["format-doc"] = mKeyBindings["format-doc"]; if ( updateConfigFile ) { - FileSystem::fileWrite( path, j.dump( 2 ) ); + data = j.dump( 2 ); + FileSystem::fileWrite( path, data ); + mConfigHash = String::hash( data ); } if ( !j.contains( "formatters" ) ) @@ -265,11 +264,11 @@ void FormatterPlugin::load( PluginManager* pluginManager ) { } if ( paths.empty() ) return; - for ( const auto& path : paths ) { + for ( const auto& fpath : paths ) { try { - loadFormatterConfig( path, mConfigPath == path ); + loadFormatterConfig( fpath, mConfigPath == fpath ); } catch ( const json::exception& e ) { - Log::error( "Parsing formatter \"%s\" failed:\n%s", path.c_str(), e.what() ); + Log::error( "Parsing formatter \"%s\" failed:\n%s", fpath.c_str(), e.what() ); } } mReady = !mFormatters.empty(); diff --git a/src/tools/ecode/plugins/pluginmanager.cpp b/src/tools/ecode/plugins/pluginmanager.cpp index 8fe4a55ef..60b2f18a0 100644 --- a/src/tools/ecode/plugins/pluginmanager.cpp +++ b/src/tools/ecode/plugins/pluginmanager.cpp @@ -496,6 +496,17 @@ PluginManager* Plugin::getManager() const { return mManager; } +PluginBase::~PluginBase() { + mShuttingDown = true; + unsubscribeFileSystemListener(); + for ( auto editor : mEditors ) { + onBeforeUnregister( editor.first ); + for ( auto listener : editor.second ) + editor.first->removeEventListener( listener ); + editor.first->unregisterPlugin( this ); + } +} + void PluginBase::onRegister( UICodeEditor* editor ) { Lock l( mMutex ); @@ -514,8 +525,9 @@ void PluginBase::onRegister( UICodeEditor* editor ) { Lock l( mMutex ); const DocEvent* docEvent = static_cast( event ); TextDocument* doc = docEvent->getDoc(); - mDocs.erase( doc ); onDocumentClosed( doc ); + onUnregisterDocument( doc ); + mDocs.erase( doc ); } } ) ); @@ -532,7 +544,10 @@ void PluginBase::onRegister( UICodeEditor* editor ) { onRegisterListeners( editor, listeners ); mEditors.insert( { editor, listeners } ); - mDocs.insert( editor->getDocumentRef().get() ); + if ( mDocs.count( editor->getDocumentRef().get() ) == 0 ) { + mDocs.insert( editor->getDocumentRef().get() ); + onRegisterDocument( editor->getDocumentRef().get() ); + } mEditorDocs[editor] = editor->getDocumentRef().get(); } diff --git a/src/tools/ecode/plugins/pluginmanager.hpp b/src/tools/ecode/plugins/pluginmanager.hpp index c107dc7d7..35021f428 100644 --- a/src/tools/ecode/plugins/pluginmanager.hpp +++ b/src/tools/ecode/plugins/pluginmanager.hpp @@ -83,7 +83,7 @@ enum class PluginMessageType { // available GetErrorOrWarning, // Request a component to provide the information of an error or warning in a // particular document location - GetDiagnostics, // Request the diagnostic information from a cursor position + GetDiagnostics, // Request the diagnostic information from a cursor position Undefined }; @@ -424,9 +424,15 @@ class Plugin : public UICodeEditorPlugin { class PluginBase : public Plugin { public: - virtual void onRegister( UICodeEditor* ); + explicit PluginBase( PluginManager* manager ) : Plugin( manager ) {} - virtual void onUnregister( UICodeEditor* ); + virtual ~PluginBase(); + + virtual void onRegister( UICodeEditor* ) override; + + virtual void onUnregister( UICodeEditor* ) override; + + virtual String::HashType getConfigFileHash() override { return mConfigHash; } protected: //! Keep track of the registered editors + all the listeners registered to each editor @@ -437,6 +443,10 @@ class PluginBase : public Plugin { Mutex mMutex; //! Keep track of the document pointer of each editor std::unordered_map mEditorDocs; + //! Keep track of the key bindings managed by the plugin + std::map mKeyBindings; /* cmd, shortcut */ + //! If the configuration is stored in a file, keep track of the config hash + String::HashType mConfigHash{ 0 }; virtual void onDocumentLoaded( TextDocument* ){}; @@ -449,6 +459,8 @@ class PluginBase : public Plugin { //! Usually used to remove keybindings in an editor virtual void onBeforeUnregister( UICodeEditor* ){}; + virtual void onRegisterDocument( TextDocument* ){}; + virtual void onUnregisterEditor( UICodeEditor* ){}; //! Usually used to unregister commands in a document diff --git a/src/tools/ecode/plugins/xmltools/xmltoolsplugin.cpp b/src/tools/ecode/plugins/xmltools/xmltoolsplugin.cpp new file mode 100644 index 000000000..89391c5af --- /dev/null +++ b/src/tools/ecode/plugins/xmltools/xmltoolsplugin.cpp @@ -0,0 +1,340 @@ +#include "xmltoolsplugin.hpp" +#include +#include +#include +#include + +using json = nlohmann::json; +#if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN || defined( __EMSCRIPTEN_PTHREADS__ ) +#define XMLTOOLS_THREADED 1 +#else +#define XMLTOOLS_THREADED 0 +#endif + +namespace ecode { + +UICodeEditorPlugin* XMLToolsPlugin::New( PluginManager* pluginManager ) { + return eeNew( XMLToolsPlugin, ( pluginManager, false ) ); +} + +UICodeEditorPlugin* XMLToolsPlugin::NewSync( PluginManager* pluginManager ) { + return eeNew( XMLToolsPlugin, ( pluginManager, true ) ); +} + +XMLToolsPlugin::XMLToolsPlugin( PluginManager* pluginManager, bool sync ) : + PluginBase( pluginManager ) { + if ( sync ) { + load( pluginManager ); + } else { +#if FORMATTER_THREADED + mThreadPool->run( [&, pluginManager] { load( pluginManager ); } ); +#else + load( pluginManager ); +#endif + } +} + +XMLToolsPlugin::~XMLToolsPlugin() { + mShuttingDown = true; + { + Lock l( mClientsMutex ); + for ( const auto& client : mClients ) + client.first->unregisterClient( client.second.get() ); + } +} + +bool XMLToolsPlugin::getHighlightMatch() const { + return mHighlightMatch; +} + +bool XMLToolsPlugin::getAutoEditMatch() const { + return mAutoEditMatch; +} + +void XMLToolsPlugin::load( PluginManager* pluginManager ) { + BoolScopedOp loading( mLoading, true ); + std::string path = pluginManager->getPluginsPath() + "xmltools.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( "XMLToolsPlugin::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( "highlight_match" ) ) + mHighlightMatch = config.value( "highlight_match", true ); + else { + config["highlight_match"] = mHighlightMatch; + updateConfigFile = true; + } + if ( config.contains( "auto_edit_match" ) ) + mAutoEditMatch = config.value( "auto_edit_match", true ); + else { + config["auto_edit_match"] = mAutoEditMatch; + updateConfigFile = true; + } + } + + if ( updateConfigFile ) { + data = j.dump( 2 ); + FileSystem::fileWrite( path, data ); + mConfigHash = String::hash( data ); + } + + fireReadyCbs(); + subscribeFileSystemListener(); +} + +void XMLToolsPlugin::onRegisterDocument( TextDocument* doc ) { + Lock l( mClientsMutex ); + mClients[doc] = std::make_unique( this, doc ); + doc->registerClient( mClients[doc].get() ); +} + +void XMLToolsPlugin::onUnregisterDocument( TextDocument* doc ) { + Lock l( mClientsMutex ); + doc->unregisterClient( mClients[doc].get() ); + mClients.erase( doc ); +} + +bool XMLToolsPlugin::isOverMatch( TextDocument* doc, const Int64& index ) const { + if ( mMatches.empty() ) + return false; + auto clientIt = mMatches.find( doc ); + if ( clientIt == mMatches.end() ) + return false; + const ClientMatch& match = clientIt->second; + if ( match.matchBracket.start().line() != index && + match.currentBracket.start().line() != index ) + return false; + if ( !match.matchBracket.inSameLine() && !match.currentBracket.inSameLine() ) + return false; + return true; +} + +static bool isClosedTag( TextDocument* doc, TextPosition start ) { + SyntaxHighlighter* highlighter = doc->getHighlighter(); + TextPosition endOfDoc = doc->endOfDoc(); + String::StringBaseType prevChar = '\0'; + do { + String::StringBaseType ch = doc->getChar( start ); + if ( ch == '>' ) { + auto tokenType = highlighter->getTokenTypeAt( start ); + if ( tokenType != "comment" && tokenType != "string" ) + return prevChar == '/'; + } + start = doc->positionOffset( start, 1 ); + prevChar = ch; + } while ( start.isValid() && start != endOfDoc ); + return false; +} + +void XMLToolsPlugin::XMLToolsClient::onDocumentTextChanged( + const DocumentContentChange& docChange ) { + if ( mAutoInserting || !mParent->getAutoEditMatch() || + !mParent->isOverMatch( mDoc, docChange.range.start().line() ) || + mDoc->getSelections().size() > 1 ) + return; + ClientMatch& match = mParent->mMatches[mDoc]; + if ( !match.currentBracket.contains( docChange.range ) ) + return; + mAutoInserting = true; + auto sel = mDoc->getSelections(); + auto diff = docChange.range.start() - match.currentBracket.start() + + ( match.currentIsClose ? TextPosition( 0, 0 ) : TextPosition( 0, 1 ) ); + auto translatedPos = match.matchBracket.normalize().start() + diff; + if ( match.currentIsClose ) { + translatedPos = mDoc->positionOffset( translatedPos, -1 ); + } + mDoc->setSelection( 0, translatedPos ); + auto translation = + docChange.range.normalized().end().column() - docChange.range.normalized().start().column(); + if ( docChange.text.empty() ) { + if ( match.currentBracket.start().line() == match.matchBracket.start().line() ) { + if ( !match.currentIsClose ) { + translatedPos = mDoc->positionOffset( translatedPos, -translation ); + } + } + mDoc->remove( + 0, { translatedPos, { translatedPos.line(), translatedPos.column() + translation } } ); + if ( mDoc->isInsertingText() ) { + mWaitingText = true; + } else { + TextRange range = match.currentIsClose ? match.currentBracket : match.matchBracket; + range.normalize(); + if ( match.isSameLine() && !match.currentIsClose ) { + range.setStart( mDoc->positionOffset( range.start(), -translation + 1 ) ); + } + auto closeText = + mDoc->getText( { range.start(), mDoc->positionOffset( range.start(), 3 ) } ); + if ( closeText == "" ) { + mJustDeletedWholeWord = true; + if ( match.isSameLine() ) { + match.currentBracket = { match.currentBracket.start(), + mDoc->positionOffset( match.currentBracket.start(), + match.currentIsClose ? 2 : 1 ) }; + match.matchBracket = { + match.matchBracket.start(), + mDoc->positionOffset( match.matchBracket.start(), + match.currentIsClose ? 1 : 1 - translation ) }; + } else { + match.currentBracket = { match.currentBracket.start(), + mDoc->positionOffset( match.currentBracket.start(), + match.currentIsClose ? 2 : 1 ) }; + match.matchBracket = { match.matchBracket.start(), + mDoc->positionOffset( match.matchBracket.start(), + match.currentIsClose ? 1 : 2 ) }; + } + } + } + } else { + if ( match.isSameLine() && !match.currentIsClose ) { + translatedPos = + mDoc->positionOffset( translatedPos, translation + docChange.text.size() ); + } + mDoc->insert( 0, translatedPos, docChange.text ); + mWaitingText = false; + if ( match.isSameLine() && match.currentIsClose ) { + for ( auto& s : sel ) { + s.start().setColumn( s.start().column() + docChange.text.size() * 2 + 1 ); + s.end().setColumn( s.end().column() + docChange.text.size() * 2 + 1 ); + } + mForceSelections = true; + mSelections = sel; + } + } + if ( !mJustDeletedWholeWord ) + mAutoInserting = false; + mDoc->setSelection( sel ); + mAutoInserting = false; +} + +void XMLToolsPlugin::XMLToolsClient::updateMatch( const TextRange& sel ) { + const auto& line = mDoc->line( mDoc->getSelection().start().line() ).getText(); + if ( mDoc->getSelection().start().column() >= (Int64)line.size() ) + return clearMatch(); + auto def = mDoc->getHighlighter()->getSyntaxDefinitionFromTextPosition( sel.start() ); + if ( !def.getAutoCloseXMLTags() ) // getAutoCloseXMLTags means that it supports XML element tags + return clearMatch(); + TextRange range = mDoc->getWordRangeInPosition( sel.start(), false ); + if ( !range.isValid() ) + return clearMatch(); + range.normalize(); + if ( range.start().column() == 0 || line.size() <= 1 ) + return clearMatch(); + if ( line[range.start().column() - 1] != '<' && line[range.start().column() - 1] != '/' && + ( range.start().column() - 2 < 0 || range.start().column() - 2 >= (Int64)line.size() || + line[range.start().column() - 2] != '<' ) ) + return clearMatch(); + bool isCloseBracket = line[range.start().column() - 1] == '/'; + if ( !isCloseBracket && isClosedTag( mDoc, range.end() ) ) + return clearMatch(); + range.start().setColumn( range.start().column() - ( isCloseBracket ? 2 : 1 ) ); + if ( mParent->mMatches.count( mDoc ) > 0 ) { + const ClientMatch& curMatch( mParent->mMatches[mDoc] ); + if ( curMatch.currentBracket == range ) + return; // Moving inside match + } + String tag( mDoc->getText( range ) ); + + TextRange found; + if ( isCloseBracket ) { + String openBracket( tag ); + openBracket.erase( 1 ); + found = mDoc->getMatchingBracket( range.start(), openBracket, tag, + TextDocument::MatchDirection::Backward ); + } else { + String closeBracket( tag ); + closeBracket.insert( 1, '/' ); + found = mDoc->getMatchingBracket( range.start(), tag, closeBracket, + TextDocument::MatchDirection::Forward ); + } + + if ( found.isValid() ) { + ClientMatch match{ range, found, isCloseBracket }; + mParent->mMatches[mDoc] = std::move( match ); + } else { + clearMatch(); + } +} + +void XMLToolsPlugin::XMLToolsClient::onDocumentSelectionChange( const TextRange& sel ) { + if ( mForceSelections ) { + mDoc->setSelection( mSelections ); + mForceSelections = false; + } + if ( mAutoInserting || mWaitingText ) + return; + if ( mJustDeletedWholeWord ) { + mJustDeletedWholeWord = false; + return; + } + if ( !mParent->getHighlightMatch() && !mParent->getAutoEditMatch() ) + return clearMatch(); + if ( mDoc->getSelection().start().line() >= (Int64)mDoc->linesCount() ) + return clearMatch(); + updateMatch( sel ); +} + +void XMLToolsPlugin::XMLToolsClient::clearMatch() { + if ( !mParent->mMatches.empty() ) + mParent->mMatches.erase( mDoc ); +} + +void XMLToolsPlugin::drawBeforeLineText( UICodeEditor* editor, const Int64& index, + Vector2f position, const Float& /*fontSize*/, + const Float& lineHeight ) { + if ( !isOverMatch( &editor->getDocument(), index ) ) + return; + Primitives p; + Color color( editor->getColorScheme().getEditorSyntaxStyle( "matching_bracket" ).color ); + Color blendedColor( Color( color, 50 ) ); + p.setColor( blendedColor ); + + const ClientMatch& match = mMatches[&editor->getDocument()]; + for ( const auto& range : { match.matchBracket, match.currentBracket } ) { + if ( range.start().line() != index || !range.inSameLine() ) + continue; + Float offset1 = editor->getXOffsetCol( range.normalized().start() ); + Float offset2 = editor->getXOffsetCol( range.normalized().end() ); + p.drawRectangle( + Rectf( { position.x + offset1, position.y }, { ( offset2 - offset1 ), lineHeight } ) ); + } +} + +void XMLToolsPlugin::minimapDrawAfterLineText( UICodeEditor* editor, const Int64& index, + const Vector2f& pos, const Vector2f& size, + const Float&, const Float& ) { + if ( !isOverMatch( &editor->getDocument(), index ) ) + return; + Primitives p; + Color color( editor->getColorScheme().getEditorSyntaxStyle( "matching_bracket" ).color ); + Color blendedColor( Color( color, 50 ) ); + p.setColor( blendedColor ); + + const ClientMatch& match = mMatches[&editor->getDocument()]; + for ( const auto& range : { match.matchBracket, match.currentBracket } ) { + if ( range.start().line() != index || !range.inSameLine() ) + continue; + p.drawRectangle( Rectf( pos, size ) ); + } +} + +} // namespace ecode diff --git a/src/tools/ecode/plugins/xmltools/xmltoolsplugin.hpp b/src/tools/ecode/plugins/xmltools/xmltoolsplugin.hpp new file mode 100644 index 000000000..66585eea0 --- /dev/null +++ b/src/tools/ecode/plugins/xmltools/xmltoolsplugin.hpp @@ -0,0 +1,112 @@ +#ifndef ECODE_XMLTOOLSPLUGIN_HPP +#define ECODE_XMLTOOLSPLUGIN_HPP + +#include "../pluginmanager.hpp" +#include +#include +#include +#include +#include +using namespace EE; +using namespace EE::System; +using namespace EE::UI; + +namespace ecode { + +class XMLToolsPlugin : public PluginBase { + public: + static PluginDefinition Definition() { + return { "xmltools", + "XML Tools", + "Simple tools to improve your XML editing experience.", + XMLToolsPlugin::New, + { 0, 0, 1 }, + XMLToolsPlugin::NewSync }; + } + + static UICodeEditorPlugin* New( PluginManager* pluginManager ); + + static UICodeEditorPlugin* NewSync( PluginManager* pluginManager ); + + virtual ~XMLToolsPlugin(); + + std::string getId() override { return Definition().id; } + + std::string getTitle() override { return Definition().name; } + + std::string getDescription() override { return Definition().description; } + + bool getHighlightMatch() const; + + bool getAutoEditMatch() const; + + void drawBeforeLineText( UICodeEditor* editor, const Int64& index, Vector2f position, + const Float& fontSize, const Float& lineHeight ) override; + + void minimapDrawAfterLineText( UICodeEditor*, const Int64&, const Vector2f&, const Vector2f&, + const Float&, const Float& ) override; + + protected: + bool mHighlightMatch{ true }; + bool mAutoEditMatch{ true }; + Mutex mClientsMutex; + + class XMLToolsClient : public TextDocument::Client { + public: + explicit XMLToolsClient( XMLToolsPlugin* parent, TextDocument* doc ) : + mDoc( doc ), mParent( parent ) {} + + virtual void onDocumentTextChanged( const DocumentContentChange& ); + virtual void onDocumentUndoRedo( const TextDocument::UndoRedo& ){}; + virtual void onDocumentCursorChange( const TextPosition& ){}; + virtual void onDocumentInterestingCursorChange( const TextPosition& ){}; + virtual void onDocumentSelectionChange( const TextRange& ); + virtual void onDocumentLineCountChange( const size_t&, const size_t& ){}; + virtual void onDocumentLineChanged( const Int64& ){}; + virtual void onDocumentSaved( TextDocument* ){}; + virtual void onDocumentClosed( TextDocument* ){}; + virtual void onDocumentDirtyOnFileSystem( TextDocument* ){}; + virtual void onDocumentMoved( TextDocument* ){}; + + protected: + TextDocument* mDoc{ nullptr }; + XMLToolsPlugin* mParent{ nullptr }; + bool mAutoInserting{ false }; + bool mWaitingText{ false }; + bool mJustDeletedWholeWord{ false }; + bool mForceSelections{ false }; + TextRanges mSelections; + + void updateMatch( const TextRange& range ); + + void clearMatch(); + }; + + using ClientsMap = std::unordered_map>; + ClientsMap mClients; + struct ClientMatch { + TextRange currentBracket; + TextRange matchBracket; + bool currentIsClose; + + bool isSameLine() const { + return currentBracket.start().line() == matchBracket.start().line(); + } + }; + using ClientsMatches = std::unordered_map; + ClientsMatches mMatches; + + XMLToolsPlugin( PluginManager* pluginManager, bool sync ); + + void load( PluginManager* pluginManager ); + + virtual void onRegisterDocument( TextDocument* doc ) override; + + virtual void onUnregisterDocument( TextDocument* doc ) override; + + bool isOverMatch( TextDocument* doc, const Int64& index ) const; +}; + +} // namespace ecode + +#endif // ECODE_XMLTOOLSPLUGIN_HPP