From c4daa308802af9ee0e0795ba8fa6f2ab5b6e440f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Fri, 20 Dec 2024 20:22:50 -0300 Subject: [PATCH] Fixes for multi-cursor. Fixes for file extensions (improperly filtering file extensions). Added very basic tests for TextDocument multi-cursor. Add .txt files into projects (register .txt extension as Plain Text). --- bin/assets/plugins/formatters.json | 10 +- bin/assets/plugins/lspclient.json | 18 +- include/eepp/ui/doc/textrange.hpp | 174 +++------------ projects/linux/ee.files | 1 + src/eepp/ui/doc/languages/c.cpp | 2 +- src/eepp/ui/doc/languages/html.cpp | 2 +- src/eepp/ui/doc/languages/json.cpp | 2 +- src/eepp/ui/doc/languages/plaintext.cpp | 24 --- src/eepp/ui/doc/languages/plaintext.hpp | 10 - src/eepp/ui/doc/syntaxdefinitionmanager.cpp | 3 +- src/eepp/ui/doc/textdocument.cpp | 42 ++-- src/eepp/ui/doc/textrange.cpp | 199 ++++++++++++++++++ src/eepp/ui/uicodeeditor.cpp | 2 + .../src/eepp/ui/doc/languages/glsl.cpp | 2 +- .../src/eepp/ui/doc/languages/ruby.cpp | 2 +- src/tests/unit_tests/textdocument.cpp | 76 +++++++ src/tests/unit_tests/utest.hpp | 52 +++++ 17 files changed, 411 insertions(+), 210 deletions(-) delete mode 100644 src/eepp/ui/doc/languages/plaintext.cpp delete mode 100644 src/eepp/ui/doc/languages/plaintext.hpp create mode 100644 src/eepp/ui/doc/textrange.cpp create mode 100644 src/tests/unit_tests/textdocument.cpp create mode 100644 src/tests/unit_tests/utest.hpp diff --git a/bin/assets/plugins/formatters.json b/bin/assets/plugins/formatters.json index 1f8516f92..35d64f5c8 100644 --- a/bin/assets/plugins/formatters.json +++ b/bin/assets/plugins/formatters.json @@ -34,13 +34,13 @@ }, { "language": "rust", - "file_patterns": ["%.rs"], + "file_patterns": ["%.rs$"], "command": "rustfmt --emit stdout --color never $FILENAME", "url": "https://rust-lang.github.io/rustfmt/" }, { "language": "go", - "file_patterns": ["%.go"], + "file_patterns": ["%.go$"], "command": "gopls format $FILENAME", "url": "https://pkg.go.dev/golang.org/x/tools/gopls" }, @@ -53,21 +53,21 @@ }, { "language": [ "xml" ], - "file_patterns": ["%.xml"], + "file_patterns": ["%.xml$"], "command": "xml", "type": "native", "url": "#native" }, { "language": "css", - "file_patterns": ["%.css"], + "file_patterns": ["%.css$"], "command": "css", "type": "native", "url": "#native" }, { "language": "zig", - "file_patterns": ["%.zig"], + "file_patterns": ["%.zig$"], "command": "zig fmt $FILENAME", "type": "inplace", "url": "https://ziglang.org" diff --git a/bin/assets/plugins/lspclient.json b/bin/assets/plugins/lspclient.json index 2bc618403..152a376d7 100644 --- a/bin/assets/plugins/lspclient.json +++ b/bin/assets/plugins/lspclient.json @@ -132,21 +132,21 @@ "name": "lua-language-server", "url": "https://github.com/sumneko/lua-language-server", "command": "lua-language-server", - "file_patterns": ["%.lua"] + "file_patterns": ["%.lua$"] }, { "language": "kotlin", "name": "kotlin-language-server", "url": "https://github.com/fwcd/kotlin-language-server", "command": "kotlin-language-server", - "file_patterns": ["%.kt"] + "file_patterns": ["%.kt$"] }, { "language": "nim", "name": "nimlsp", "url": "https://github.com/PMunch/nimlsp", "command": "nimlsp", - "file_patterns": ["%.nim"] + "file_patterns": ["%.nim$"] }, { "language": "ruby", @@ -154,28 +154,28 @@ "url": "https://solargraph.org", "command": "solargraph stdio", "rootIndicationFileNames": ["Gemfile", "Gemfile.lock", "gems.rb", "gems.lock", "Rakefile"], - "file_patterns": ["%.rb"] + "file_patterns": ["%.rb$"] }, { "language": "yaml", "name": "yaml-language-server", "url": "https://github.com/redhat-developer/yaml-language-server", "command": "yaml-language-server --stdio", - "file_patterns": ["%.yaml", "%.yml"] + "file_patterns": ["%.yaml$", "%.yml$"] }, { "language": "dart", "name": "dart language-server", "url": "https://github.com/dart-lang/sdk/blob/main/pkg/analysis_server/tool/lsp_spec", "command": "dart language-server --client-id ecode", - "file_patterns": ["%.dart"] + "file_patterns": ["%.dart$"] }, { "language": "shellscript", "name": "bash-language-server", "url": "https://github.com/bash-lsp/bash-language-server", "command": "bash-language-server start", - "file_patterns": ["%.sh", "%.bash"] + "file_patterns": ["%.sh$", "%.bash$"] }, { "language": "html", @@ -317,7 +317,7 @@ "name": "glsl_analyzer", "url": "https://github.com/nolanderc/glsl_analyzer", "command": "glsl_analyzer", - "file_patterns": ["%.glsl$", "%.frag$", "%.vert$", "%.fs$", "%.vs$", "%.tesc", "%.tese"] + "file_patterns": ["%.glsl$", "%.frag$", "%.vert$", "%.fs$", "%.vs$", "%.tesc$", "%.tese$"] }, { "language": "vala", @@ -366,7 +366,7 @@ "name": "LanguageServer.jl", "url": "https://github.com/julia-vscode/LanguageServer.jl", "command": "julia --project=\"$HOME/.julia/packages/LanguageServer/Fwm1f/src/LanguageServer.jl\" -e \"using LanguageServer; runserver()\"", - "file_patterns": ["%.jl"] + "file_patterns": ["%.jl$"] }, { "language": "fortran", diff --git a/include/eepp/ui/doc/textrange.hpp b/include/eepp/ui/doc/textrange.hpp index 667c44aea..c302f0873 100644 --- a/include/eepp/ui/doc/textrange.hpp +++ b/include/eepp/ui/doc/textrange.hpp @@ -2,23 +2,19 @@ #define EE_UI_DOC_TEXTRANGE_HPP #include -#include #include namespace EE { namespace UI { namespace Doc { class EE_API TextRange { public: - TextRange() {} - TextRange( const TextPosition& start, const TextPosition& end ) : - mStart( start ), mEnd( end ) {} + TextRange(); - bool isValid() const { return mStart.isValid() && mEnd.isValid(); } + TextRange( const TextPosition& start, const TextPosition& end ); - void clear() { - mStart = {}; - mEnd = {}; - } + bool isValid() const; + + void clear(); TextPosition& start() { return mStart; } @@ -28,14 +24,9 @@ class EE_API TextRange { const TextPosition& end() const { return mEnd; } - TextRange normalized() const { return TextRange( normalizedStart(), normalizedEnd() ); } + TextRange normalized() const; - TextRange& normalize() { - auto normalize( normalized() ); - mStart = normalize.start(); - mEnd = normalize.end(); - return *this; - } + TextRange& normalize(); void reverse() { std::swap( mEnd, mStart ); } @@ -45,10 +36,7 @@ class EE_API TextRange { void setEnd( const TextPosition& position ) { mEnd = position; } - void set( const TextPosition& start, const TextPosition& end ) { - mStart = start; - mEnd = end; - } + void set( const TextPosition& start, const TextPosition& end ); bool operator==( const TextRange& other ) const { return mStart == other.mStart && mEnd == other.mEnd; @@ -90,21 +78,9 @@ class EE_API TextRange { 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() ) ) ) - return false; - if ( !( position.line() < mEnd.line() || - ( position.line() == mEnd.line() && position.column() <= mEnd.column() ) ) ) - return false; - return true; - } + bool contains( const TextPosition& position ) const; - bool intersectsLineRange( const TextRange& range ) const { - eeASSERT( range.isNormalized() ); - return mStart.line() <= static_cast( range.end().line() ) && - static_cast( range.start().line() ) <= mEnd.line(); - } + bool intersectsLineRange( const TextRange& range ) const; template bool intersectsLineRange( T fromLine, T toLine ) const { return mStart.line() <= static_cast( toLine ) && @@ -116,44 +92,25 @@ class EE_API TextRange { static_cast( range.first ) <= mEnd.line(); } - bool containsLine( const Int64& line ) const { - return line >= mStart.line() && line <= mEnd.line(); - } + bool containsLine( const Int64& line ) const; - bool contains( const TextRange& range ) const { - return range.start() >= start() && range.end() <= end(); - } + bool contains( const TextRange& range ) const; + + bool intersects( const TextRange& range ) const; + + TextRange merge( const TextRange& range ) const; bool hasSelection() const { return isValid() && mStart != mEnd; } bool inSameLine() const { return isValid() && mStart.line() == mEnd.line(); } - Int64 height() const { - if ( mEnd.line() > mStart.line() ) - return mEnd.line() - mStart.line() + 1; - return mStart.line() - mStart.line() + 1; - } + Int64 height() const; - Int64 length() const { - if ( !inSameLine() ) - return 0; - if ( mEnd.column() > mStart.column() ) - return mEnd.column() - mStart.column(); - return mStart.column() - mEnd.column(); - } + Int64 length() const; - std::string toString() const { - return String::format( "%s - %s", mStart.toString().c_str(), mEnd.toString().c_str() ); - } + std::string toString() const; - static TextRange fromString( const std::string& range ) { - auto split = String::split( range, "-" ); - if ( split.size() == 2 ) { - return { TextPosition::fromString( String::trim( split[0] ) ), - TextPosition::fromString( String::trim( split[1] ) ) }; - } - return {}; - } + static TextRange fromString( const std::string& range ); bool isNormalized() const { return mStart <= mEnd; } @@ -168,96 +125,31 @@ class EE_API TextRange { class EE_API TextRanges : public std::vector { public: - TextRanges() {} + TextRanges(); - TextRanges( const std::vector& ranges ) : std::vector( ranges ) {} + TextRanges( const std::vector& ranges ); - TextRanges( const TextRange& ranges ) : std::vector( { ranges } ) {} + TextRanges( const TextRange& ranges ); - bool isSorted() const { return mIsSorted; } + bool isSorted() const; - bool isValid() const { - for ( const auto& selection : *this ) { - if ( !selection.isValid() ) - return false; - } - return true; - } - - bool exists( const TextRange& range ) const { - if ( !mIsSorted ) - return std::find( begin(), end(), range ) != end(); - return std::binary_search( begin(), end(), range ); - } + bool isValid() const; - size_t findIndex( const TextRange& range ) const { - if ( !mIsSorted ) { - auto it = std::find( begin(), end(), range ); - return it != end() ? std::distance( begin(), it ) : static_cast( -1 ); - } else { - auto it = std::lower_bound( begin(), end(), range ); - return ( it != end() && *it == range ) ? std::distance( begin(), it ) - : static_cast( -1 ); - } - } + bool exists( const TextRange& range ) const; - bool hasSelection() const { - for ( const auto& r : *this ) - if ( r.hasSelection() ) - return true; - return false; - } - - void sort() { - std::sort( begin(), end() ); - setSorted(); - } + size_t findIndex( const TextRange& range ) const; - void setSorted() { mIsSorted = true; } + bool hasSelection() const; - bool merge() { - if ( size() <= 1 ) - return false; + void sort(); - if ( !mIsSorted ) - sort(); + void setSorted(); - auto itUnique = std::unique( - begin(), end(), []( const TextRange& a, const TextRange& b ) { return a == b; } ); + bool merge(); - bool merged = itUnique != end(); - erase( itUnique, end() ); + std::string toString() const; - auto it = begin(); - while ( it != end() ) { - auto next = std::next( it ); - while ( next != end() && it != end() && next->contains( *it ) ) { - erase( it ); - it = std::prev( next ); - } - it = next; - } - - return merged; - } - - std::string toString() const { - std::string str; - for ( size_t i = 0; i < size(); ++i ) { - str += ( *this )[i].toString(); - if ( i != size() - 1 ) - str += ";"; - } - return str; - } - - static TextRanges fromString( const std::string& str ) { - auto rangesStr = String::split( str, ';' ); - TextRanges ranges; - for ( const auto& rangeStr : rangesStr ) - ranges.emplace_back( TextRange::fromString( rangeStr ) ); - return ranges; - } + static TextRanges fromString( const std::string& str ); protected: bool mIsSorted{ false }; diff --git a/projects/linux/ee.files b/projects/linux/ee.files index 9ce85b4ac..1430b70a6 100644 --- a/projects/linux/ee.files +++ b/projects/linux/ee.files @@ -1127,6 +1127,7 @@ ../../src/eepp/ui/doc/syntaxtokenizer.cpp ../../src/eepp/ui/doc/textdocument.cpp ../../src/eepp/ui/doc/textformat.cpp +../../src/eepp/ui/doc/textrange.cpp ../../src/eepp/ui/doc/textundostack.cpp ../../src/eepp/ui/doc/documentview.cpp ../../src/eepp/ui/keyboardshortcut.cpp diff --git a/src/eepp/ui/doc/languages/c.cpp b/src/eepp/ui/doc/languages/c.cpp index 0342ecb79..e56732eb6 100644 --- a/src/eepp/ui/doc/languages/c.cpp +++ b/src/eepp/ui/doc/languages/c.cpp @@ -8,7 +8,7 @@ void addC() { auto& sd = SyntaxDefinitionManager::instance()->add( { "C", - { "%.c$", "%.C", "%.h$", "%.icc" }, + { "%.c$", "%.C$", "%.h$", "%.icc$" }, { { { "//.-\n" }, "comment" }, { { "/%*", "%*/" }, "comment" }, diff --git a/src/eepp/ui/doc/languages/html.cpp b/src/eepp/ui/doc/languages/html.cpp index 1de3afb28..d781897ab 100644 --- a/src/eepp/ui/doc/languages/html.cpp +++ b/src/eepp/ui/doc/languages/html.cpp @@ -9,7 +9,7 @@ void addHTML() { ->add( { "HTML", - { "%.[mp]?html?$", "%.handlebars" }, + { "%.[mp]?html?$", "%.handlebars$" }, { { { "<%s*[sS][cC][rR][iI][pP][tT]%s+[tT][yY][pP][eE]%s*=%s*['\"]%a+/" "[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]['\"]%s*>", diff --git a/src/eepp/ui/doc/languages/json.cpp b/src/eepp/ui/doc/languages/json.cpp index 0e99f456e..202f319b6 100644 --- a/src/eepp/ui/doc/languages/json.cpp +++ b/src/eepp/ui/doc/languages/json.cpp @@ -8,7 +8,7 @@ void addJSON() { auto& sd = SyntaxDefinitionManager::instance()->add( { "JSON", - { "%.json$", "%.cson$", "%.webmanifest" }, + { "%.json$", "%.cson$", "%.webmanifest$" }, { { { "(%b\"\")(:)" }, { "normal", "keyword", "operator" } }, { { "\"", "\"", "\\" }, "string" }, diff --git a/src/eepp/ui/doc/languages/plaintext.cpp b/src/eepp/ui/doc/languages/plaintext.cpp deleted file mode 100644 index e43c26250..000000000 --- a/src/eepp/ui/doc/languages/plaintext.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include -#include - -namespace EE { namespace UI { namespace Doc { namespace Language { - -void addPlaintext() { - -SyntaxDefinitionManager::instance()->add( - -{"Plain Text", -{}, -{ - -}, -{ - -}, -"", -{}, -"plaintext" -}); -} - -}}}} // namespace EE::UI::Doc::Language diff --git a/src/eepp/ui/doc/languages/plaintext.hpp b/src/eepp/ui/doc/languages/plaintext.hpp deleted file mode 100644 index 873b9ce98..000000000 --- a/src/eepp/ui/doc/languages/plaintext.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef EE_UI_DOC_Plaintext -#define EE_UI_DOC_Plaintext - -namespace EE { namespace UI { namespace Doc { namespace Language { - -extern void addPlaintext(); - -}}}} - -#endif diff --git a/src/eepp/ui/doc/syntaxdefinitionmanager.cpp b/src/eepp/ui/doc/syntaxdefinitionmanager.cpp index 816cf22cd..e92aa00b0 100644 --- a/src/eepp/ui/doc/syntaxdefinitionmanager.cpp +++ b/src/eepp/ui/doc/syntaxdefinitionmanager.cpp @@ -37,7 +37,8 @@ SyntaxDefinitionManager::createSingleton( std::size_t reserveSpaceForLanguages ) } static void addPlainText() { - SyntaxDefinitionManager::instance()->add( { "Plain Text", {}, {}, {}, "", {}, "plaintext" } ); + SyntaxDefinitionManager::instance()->add( + { "Plain Text", { "%.txt$" }, {}, {}, "", {}, "plaintext" } ); } // Syntax definitions can be directly converted from the lite (https://github.com/rxi/lite) and diff --git a/src/eepp/ui/doc/textdocument.cpp b/src/eepp/ui/doc/textdocument.cpp index 9b53133f0..cc8538ebe 100644 --- a/src/eepp/ui/doc/textdocument.cpp +++ b/src/eepp/ui/doc/textdocument.cpp @@ -59,7 +59,9 @@ TextDocument::~TextDocument() { Lock l( mLoadingMutex ); } - { Lock l( mClientsMutex ); } + { + Lock l( mClientsMutex ); + } // Loading has been stopped while ( mLoadingAsync ) { @@ -1305,13 +1307,13 @@ size_t TextDocument::remove( const size_t& cursorIdx, TextRange range, mLines.emplace_back( String( "\n" ) ); if ( mSelection.size() > 1 ) { - auto ranNorm( originalRange.normalized() ); - Int64 lineRem = ranNorm.end().line() - ranNorm.start().line(); + auto oriNm( originalRange.normalized() ); + Int64 lineRem = oriNm.end().line() - oriNm.start().line(); size_t curIdx = 0; - Int64 colRem = ranNorm.start().line() == ranNorm.end().line() - ? ranNorm.end().column() - ranNorm.start().column() - : ranNorm.end().column(); + Int64 colRem = oriNm.start().line() == oriNm.end().line() + ? oriNm.end().column() - oriNm.start().column() + : oriNm.end().column(); for ( auto& sel : mSelection ) { if ( curIdx == cursorIdx ) { @@ -1319,9 +1321,10 @@ size_t TextDocument::remove( const size_t& cursorIdx, TextRange range, continue; } - auto selNorm( sel.normalized() ); + auto selNm( sel.normalized() ); + auto selOri( sel ); - if ( selNorm.start().line() < ranNorm.end().line() ) { + if ( selNm.start().line() < oriNm.end().line() ) { curIdx++; continue; } @@ -1329,13 +1332,22 @@ size_t TextDocument::remove( const size_t& cursorIdx, TextRange range, if ( lineRem != 0 ) { sel.start().setLine( sel.start().line() - lineRem ); sel.end().setLine( sel.end().line() - lineRem ); + } + + if ( selOri.start().line() == oriNm.end().line() && + selOri.start().column() >= oriNm.end().column() ) { + if ( sel.start().line() != selOri.start().line() ) + sel.start().setColumn( oriNm.start().column() + sel.start().column() - colRem ); + else + sel.start().setColumn( sel.start().column() - colRem ); + } - sel.start().setColumn( ranNorm.start().column() + sel.start().column() - colRem ); - sel.end().setColumn( ranNorm.start().column() + sel.end().column() - colRem ); - } else if ( selNorm.end().line() == ranNorm.end().line() && - ranNorm.end().column() <= selNorm.start().column() ) { - sel.start().setColumn( sel.start().column() - colRem ); - sel.end().setColumn( sel.end().column() - colRem ); + if ( selOri.end().line() == oriNm.end().line() && + selOri.end().column() >= oriNm.end().column() ) { + if ( selOri.end().line() != sel.end().line() ) + sel.end().setColumn( oriNm.start().column() + sel.end().column() - colRem ); + else + sel.end().setColumn( sel.end().column() - colRem ); } sel = sanitizeRange( sel ); @@ -1912,7 +1924,7 @@ void TextDocument::deleteToNextChar() { void TextDocument::deleteToEndOfLine() { for ( size_t i = 0; i < mSelection.size(); ++i ) - deleteTo( i, endOfLine( getSelectionIndex( i ).start() )); + deleteTo( i, endOfLine( getSelectionIndex( i ).start() ) ); mergeSelection(); } diff --git a/src/eepp/ui/doc/textrange.cpp b/src/eepp/ui/doc/textrange.cpp new file mode 100644 index 000000000..1506bef67 --- /dev/null +++ b/src/eepp/ui/doc/textrange.cpp @@ -0,0 +1,199 @@ +#include +#include + +namespace EE { namespace UI { namespace Doc { + +TextRange::TextRange() {} + +TextRange::TextRange( const TextPosition& start, const TextPosition& end ) : + mStart( start ), mEnd( end ) {} + +bool TextRange::isValid() const { + return mStart.isValid() && mEnd.isValid(); +} + +void TextRange::clear() { + mStart = {}; + mEnd = {}; +} + +TextRange TextRange::normalized() const { + return TextRange( normalizedStart(), normalizedEnd() ); +} + +TextRange& TextRange::normalize() { + auto normalize( normalized() ); + mStart = normalize.start(); + mEnd = normalize.end(); + return *this; +} + +void TextRange::set( const TextPosition& start, const TextPosition& end ) { + mStart = start; + mEnd = end; +} + +bool TextRange::contains( const TextPosition& position ) const { + if ( !( position.line() > mStart.line() || + ( position.line() == mStart.line() && position.column() >= mStart.column() ) ) ) + return false; + if ( !( position.line() < mEnd.line() || + ( position.line() == mEnd.line() && position.column() <= mEnd.column() ) ) ) + return false; + return true; +} + +bool TextRange::intersectsLineRange( const TextRange& range ) const { + eeASSERT( range.isNormalized() ); + return mStart.line() <= static_cast( range.end().line() ) && + static_cast( range.start().line() ) <= mEnd.line(); +} + +bool TextRange::containsLine( const Int64& line ) const { + return line >= mStart.line() && line <= mEnd.line(); +} + +bool TextRange::contains( const TextRange& range ) const { + return range.start() >= start() && range.end() <= end(); +} + +bool TextRange::intersects( const TextRange& range ) const { + return range.start() <= end() && range.end() >= start(); +} + +TextRange TextRange::merge( const TextRange& range ) const { + bool wasNormalized = mStart <= mEnd; + TextRange normalizedThis = this->normalized(); + TextRange normalizedRange = range.normalized(); + TextPosition mergedStart = normalizedThis.start() < normalizedRange.start() + ? normalizedThis.start() + : normalizedRange.start(); + TextPosition mergedEnd = + normalizedThis.end() > normalizedRange.end() ? normalizedThis.end() : normalizedRange.end(); + return TextRange( wasNormalized ? mergedStart : mergedEnd, + wasNormalized ? mergedEnd : mergedStart ); +} + +Int64 TextRange::height() const { + if ( mEnd.line() > mStart.line() ) + return mEnd.line() - mStart.line() + 1; + return mStart.line() - mStart.line() + 1; +} + +Int64 TextRange::length() const { + if ( !inSameLine() ) + return 0; + if ( mEnd.column() > mStart.column() ) + return mEnd.column() - mStart.column(); + return mStart.column() - mEnd.column(); +} + +std::string TextRange::toString() const { + return String::format( "%s - %s", mStart.toString().c_str(), mEnd.toString().c_str() ); +} + +TextRange TextRange::fromString( const std::string& range ) { + auto split = String::split( range, "-" ); + if ( split.size() == 2 ) { + return { TextPosition::fromString( String::trim( split[0] ) ), + TextPosition::fromString( String::trim( split[1] ) ) }; + } + return {}; +} + +TextRanges::TextRanges() {} + +TextRanges::TextRanges( const std::vector& ranges ) : std::vector( ranges ) {} + +TextRanges::TextRanges( const TextRange& ranges ) : std::vector( { ranges } ) {} + +bool TextRanges::isSorted() const { + return mIsSorted; +} + +bool TextRanges::isValid() const { + for ( const auto& selection : *this ) { + if ( !selection.isValid() ) + return false; + } + return true; +} + +bool TextRanges::exists( const TextRange& range ) const { + if ( !mIsSorted ) + return std::find( begin(), end(), range ) != end(); + return std::binary_search( begin(), end(), range ); +} + +size_t TextRanges::findIndex( const TextRange& range ) const { + if ( !mIsSorted ) { + auto it = std::find( begin(), end(), range ); + return it != end() ? std::distance( begin(), it ) : static_cast( -1 ); + } else { + auto it = std::lower_bound( begin(), end(), range ); + return ( it != end() && *it == range ) ? std::distance( begin(), it ) + : static_cast( -1 ); + } +} + +bool TextRanges::hasSelection() const { + for ( const auto& r : *this ) + if ( r.hasSelection() ) + return true; + return false; +} + +void TextRanges::sort() { + std::sort( begin(), end() ); + setSorted(); +} + +void TextRanges::setSorted() { + mIsSorted = true; +} + +bool TextRanges::merge() { + if ( size() <= 1 ) + return false; + + if ( !mIsSorted ) + sort(); + + auto itUnique = std::unique( begin(), end(), + []( const TextRange& a, const TextRange& b ) { return a == b; } ); + + bool merged = itUnique != end(); + erase( itUnique, end() ); + + for ( auto it = begin(); it != end(); ++it ) { + auto next = std::next( it ); + while ( next != end() && + ( it->intersects( *next ) || it->contains( *next ) || next->contains( *it ) ) ) { + *it = it->merge( *next ); + next = erase( next ); + merged = true; + } + } + + return merged; +} + +std::string TextRanges::toString() const { + std::string str; + for ( size_t i = 0; i < size(); ++i ) { + str += ( *this )[i].toString(); + if ( i != size() - 1 ) + str += ";"; + } + return str; +} + +TextRanges TextRanges::fromString( const std::string& str ) { + auto rangesStr = String::split( str, ';' ); + TextRanges ranges; + for ( const auto& rangeStr : rangesStr ) + ranges.emplace_back( TextRange::fromString( rangeStr ) ); + return ranges; +} + +}}} // namespace EE::UI::Doc diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 7cc87254c..df22fe039 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -3130,6 +3130,7 @@ void UICodeEditor::selectToPreviousLine() { TextPosition position = mDoc->getSelectionIndex( i ).start(); mDoc->selectTo( i, moveToLineOffset( position, -1 ) ); } + mDoc->mergeSelection(); } void UICodeEditor::selectToNextLine() { @@ -3141,6 +3142,7 @@ void UICodeEditor::selectToNextLine() { mDoc->selectTo( i, moveToLineOffset( position, 1 ) ); } } + mDoc->mergeSelection(); } void UICodeEditor::moveScrollUp() { diff --git a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/glsl.cpp b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/glsl.cpp index baf3b0a72..1517a9d71 100644 --- a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/glsl.cpp +++ b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/glsl.cpp @@ -8,7 +8,7 @@ void addGLSL() { auto& sd = SyntaxDefinitionManager::instance()->add( { "GLSL", - { "%.glsl$", "%.frag$", "%.vert$", "%.fs$", "%.vs$", "%.tesc", "%.tese" }, + { "%.glsl$", "%.frag$", "%.vert$", "%.fs$", "%.vs$", "%.tesc$", "%.tese$" }, { { { "//.-\n" }, "comment" }, { { "/%*", "%*/" }, "comment" }, diff --git a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/ruby.cpp b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/ruby.cpp index b76f104a5..24241e1e5 100644 --- a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/ruby.cpp +++ b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/ruby.cpp @@ -8,7 +8,7 @@ void addRuby() { auto& sd = SyntaxDefinitionManager::instance()->add( { "Ruby", - { "%.rb", "%.gemspec", "%.ruby" }, + { "%.rb$", "%.gemspec$", "%.ruby$" }, { { { "\"", "\"", "\\" }, "string" }, { { "'", "'", "\\" }, "string" }, diff --git a/src/tests/unit_tests/textdocument.cpp b/src/tests/unit_tests/textdocument.cpp new file mode 100644 index 000000000..41aa57e7d --- /dev/null +++ b/src/tests/unit_tests/textdocument.cpp @@ -0,0 +1,76 @@ +#include "utest.hpp" +#include +#include +#include + +using namespace EE::UI::Doc; +using namespace EE::System; + +UTEST( TextDocument, multicursor ) { + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + TextDocument doc; + doc.loadFromFile( "assets/textformat/english.utf8.lf.nobom.txt" ); + EXPECT_EQ( doc.linesCount() > 0, true ); + + // Same line delete + for ( int op = 0; op < 2; op++ ) { + doc.setSelection( { { 0, 4 }, { 0, 5 } } ); + + EXPECT_STRINGEQ( "a", doc.getSelectedText() ); + + // Select all "a" from first line + doc.selectWord(); + doc.selectWord(); + doc.selectWord(); + + switch ( op ) { + case 0: + doc.deleteToPreviousChar(); + break; + case 1: + doc.deleteToNextChar(); + break; + } + + EXPECT_STRINGEQ( "It ws bright cold dy in April, nd the clocks were striking thirteen.\n", + doc.line( 0 ).getText() ); + + doc.resetSelection( TextRange{ { 0, 0 }, { 0, 0 } } ); + doc.undo(); + } + + // Multi-line delete + for ( int op = 0; op < 4; op++ ) { + TextRanges ranges( + { TextRange( { 3, 65 }, { 4, 11 } ), TextRange( { 17, 66 }, { 18, 67 } ) } ); + + if ( op >= 2 ) + for ( auto& range : ranges ) + range.reverse(); + + doc.resetSelection( ranges ); + + switch ( op % 2 ) { + case 0: + doc.deleteToPreviousChar(); + break; + case 1: + doc.deleteToNextChar(); + break; + } + + EXPECT_STRINGEQ( "though not quickly enough to prevent a swirl of gritty dust from him.\n", + doc.line( 3 ).getText() ); + EXPECT_STRINGEQ( "one of those pictures which are so contrived that the eyes follow ran.\n", + doc.line( 16 ).getText() ); + EXPECT_STDSTREQ( TextRange( { 3, 65 }, { 3, 65 } ).toString(), + doc.getSelectionIndex( 0 ).toString() ); + EXPECT_STDSTREQ( TextRange( { 16, 66 }, { 16, 66 } ).toString(), + doc.getSelectionIndex( 1 ).toString() ); + + doc.undo(); + } + + doc.resetUndoRedo(); + doc.resetSelection( TextRange{ { 0, 0 }, { 0, 0 } } ); +} diff --git a/src/tests/unit_tests/utest.hpp b/src/tests/unit_tests/utest.hpp new file mode 100644 index 000000000..3f8d75ef1 --- /dev/null +++ b/src/tests/unit_tests/utest.hpp @@ -0,0 +1,52 @@ +#pragma once +#include "utest.h" + +#define UTEST_STDSTREQ(x, y, msg, is_assert) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const std::string xEval = (x); \ + const std::string yEval = (y); \ + if (xEval != yEval) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : \"%s\"\n", xEval.c_str()); \ + UTEST_PRINTF(" Actual : \"%s\"\n", yEval.c_str()); \ + if (strlen(msg) > 0) { \ + UTEST_PRINTF(" Message : %s\n", msg); \ + } \ + *utest_result = UTEST_TEST_FAILURE; \ + if (is_assert) { \ + return; \ + } \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define EXPECT_STDSTREQ(x, y) UTEST_STDSTREQ(x, y, "", 0) +#define EXPECT_STDSTREQ_MSG(x, y, msg) UTEST_STDSTREQ(x, y, msg, 0) +#define ASSERT_STDSTREQ(x, y) UTEST_STDSTREQ(x, y, "", 1) +#define ASSERT_STDSTREQ_MSG(x, y, msg) UTEST_STDSTREQ(x, y, msg, 1) + +#define UTEST_STRINGEQ(x, y, msg, is_assert) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const String xEval = (x); \ + const String yEval = (y); \ + if (xEval != yEval) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : \"%s\"\n", xEval.toUtf8().c_str()); \ + UTEST_PRINTF(" Actual : \"%s\"\n", yEval.toUtf8().c_str()); \ + if (strlen(msg) > 0) { \ + UTEST_PRINTF(" Message : %s\n", msg); \ + } \ + *utest_result = UTEST_TEST_FAILURE; \ + if (is_assert) { \ + return; \ + } \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define EXPECT_STRINGEQ(x, y) UTEST_STRINGEQ(x, y, "", 0) +#define EXPECT_STRINGEQ_MSG(x, y, msg) UTEST_STRINGEQ(x, y, msg, 0) +#define ASSERT_STRINGEQ(x, y) UTEST_STRINGEQ(x, y, "", 1) +#define ASSERT_STRINGEQ_MSG(x, y, msg) UTEST_STRINGEQ(x, y, msg, 1)