ecode: Added XML Tools plugin, currently provides highlight of matching xml tags and auto edit of xml tag name. Some minor fixes are still pending.

This commit is contained in:
Martín Lucas Golini
2023-07-09 23:09:59 -03:00
parent 95e938264f
commit c2e8a55bfa
11 changed files with 613 additions and 105 deletions

View File

@@ -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

View File

@@ -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<std::pair<String::StringBaseType, String::StringBaseType>> mAutoCloseBracketsPairs;
Uint32 mIndentWidth{ 4 };
IndentType mIndentType{ IndentType::IndentTabs };

View File

@@ -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() ) ) )

View File

@@ -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

View File

@@ -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<bool> 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;
}
}

View File

@@ -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<std::string> 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() ) ) {

View File

@@ -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();

View File

@@ -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<const DocEvent*>( 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();
}

View File

@@ -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<UICodeEditor*, TextDocument*> mEditorDocs;
//! Keep track of the key bindings managed by the plugin
std::map<std::string, std::string> 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

View File

@@ -0,0 +1,340 @@
#include "xmltoolsplugin.hpp"
#include <eepp/graphics/primitives.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/scopedop.hpp>
#include <nlohmann/json.hpp>
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<XMLToolsClient>( 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

View File

@@ -0,0 +1,112 @@
#ifndef ECODE_XMLTOOLSPLUGIN_HPP
#define ECODE_XMLTOOLSPLUGIN_HPP
#include "../pluginmanager.hpp"
#include <eepp/config.hpp>
#include <eepp/system/mutex.hpp>
#include <eepp/system/threadpool.hpp>
#include <eepp/ui/uicodeeditor.hpp>
#include <set>
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<TextDocument*, std::unique_ptr<XMLToolsClient>>;
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<TextDocument*, ClientMatch>;
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