From cebde979c9b91e3fa9fa54b2a07ffbfede4d7186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Fri, 6 Mar 2026 19:14:38 -0300 Subject: [PATCH] Fixes and sanity checks for issue SpartanJ/ecode#836. Added several tests that triggered the crashes. --- include/eepp/system/singleton.hpp | 2 - include/eepp/ui/doc/documentview.hpp | 4 + src/eepp/ui/doc/documentview.cpp | 65 +++- src/eepp/ui/doc/foldrangeservice.cpp | 52 ++- .../src/eepp/ui/doc/languages/objective-c.cpp | 1 + .../eepp/ui/doc/languages/objective-cpp.cpp | 2 + .../ui/doc/languagessyntaxhighlighting.cpp | 13 +- src/tests/unit_tests/uicodeeditor_test.cpp | 311 ++++++++++++++++++ 8 files changed, 420 insertions(+), 30 deletions(-) create mode 100644 src/tests/unit_tests/uicodeeditor_test.cpp diff --git a/include/eepp/system/singleton.hpp b/include/eepp/system/singleton.hpp index ce966ca37..c450f78da 100644 --- a/include/eepp/system/singleton.hpp +++ b/include/eepp/system/singleton.hpp @@ -33,7 +33,6 @@ } \ \ T* T::existsSingleton() { \ - Lock l( ms_mutex ); \ return ms_singleton; \ } \ \ @@ -62,7 +61,6 @@ template class Singleton { public: /** Get the singleton pointer (without instance verification) */ static T* existsSingleton() { - Lock l( ms_mutex ); return ms_singleton; } diff --git a/include/eepp/ui/doc/documentview.hpp b/include/eepp/ui/doc/documentview.hpp index 275caea6f..92ea5a360 100644 --- a/include/eepp/ui/doc/documentview.hpp +++ b/include/eepp/ui/doc/documentview.hpp @@ -139,6 +139,10 @@ class EE_API DocumentView { bool usesTabStops() const { return mConfig.tabStops; } + const std::vector getDocLineToVisibleIndex() const { return mDocLineToVisibleIndex; } + + const std::vector getVisibleLinesOffset() const { return mVisibleLinesOffset; } + protected: std::shared_ptr mDoc; FontStyleConfig mFontStyle; diff --git a/src/eepp/ui/doc/documentview.cpp b/src/eepp/ui/doc/documentview.cpp index a974d636b..42ed1a100 100644 --- a/src/eepp/ui/doc/documentview.cpp +++ b/src/eepp/ui/doc/documentview.cpp @@ -345,9 +345,16 @@ Float DocumentView::getWhiteSpaceWidth() const { } void DocumentView::updateCache( Int64 fromLine, Int64 toLine, Int64 numLines ) { - if ( 0 == mMaxWidth || isOneToOne() ) + if ( 0 == mMaxWidth || isOneToOne() || !mDoc ) return; + // Safety check: ensure fromLine and toLine are within bounds of the old state + if ( fromLine < 0 || fromLine >= (Int64)mVisibleLinesOffset.size() || + toLine < 0 || toLine >= (Int64)mVisibleLinesOffset.size() || fromLine > toLine ) { + invalidateCache(); + return; + } + // Unfold ANY modification over a folded range if ( numLines < 0 ) { auto foldedRegions = intersectsFoldedRegions( { { fromLine, 0 }, { toLine, 0 } } ); @@ -362,6 +369,15 @@ void DocumentView::updateCache( Int64 fromLine, Int64 toLine, Int64 numLines ) { Int64 oldIdxFrom = static_cast( toVisibleIndex( fromLine, false ) ); Int64 oldIdxTo = static_cast( toVisibleIndex( toLine, true ) ); + if ( oldIdxFrom == static_cast( VisibleIndex::invalid ) || + oldIdxTo == static_cast( VisibleIndex::invalid ) || + oldIdxFrom > oldIdxTo || + oldIdxFrom >= (Int64)mVisibleLines.size() || + oldIdxTo >= (Int64)mVisibleLines.size() ) { + invalidateCache(); + return; + } + auto visibleLinesCount = mVisibleLines.size(); // Remove old visible lines @@ -383,6 +399,10 @@ void DocumentView::updateCache( Int64 fromLine, Int64 toLine, Int64 numLines ) { // Recompute line breaks auto netLines = toLine + numLines; auto idxOffset = oldIdxFrom; + + if ( netLines >= (Int64)mDocLineToVisibleIndex.size() ) + mDocLineToVisibleIndex.resize( netLines + 1, static_cast( VisibleIndex::invalid ) ); + for ( auto i = fromLine; i <= netLines; i++ ) { if ( isFolded( i ) ) { mVisibleLinesOffset.insert( @@ -423,18 +443,25 @@ void DocumentView::recomputeDocLineToVisibleIndex( Int64 fromVisibleIndex, bool Int64 visibleLinesCount = mVisibleLines.size(); if ( ensureDocSize ) mDocLineToVisibleIndex.resize( mDoc->linesCount() ); + + if ( fromVisibleIndex < 0 || fromVisibleIndex >= visibleLinesCount ) + return; + Int64 previousLineIdx = mVisibleLines[fromVisibleIndex].line(); for ( Int64 visibleIdx = fromVisibleIndex; visibleIdx < visibleLinesCount; visibleIdx++ ) { const auto& visibleLine = mVisibleLines[visibleIdx]; if ( visibleLine.column() == 0 ) { // Non-contiguous lines means hidden lines if ( visibleLine.line() - previousLineIdx > 1 ) { - for ( Int64 i = previousLineIdx + 1; i < visibleLine.line(); i++ ) - mDocLineToVisibleIndex[i] = static_cast( VisibleIndex::invalid ); + for ( Int64 i = previousLineIdx + 1; i < visibleLine.line(); i++ ) { + if ( i < (Int64)mDocLineToVisibleIndex.size() ) + mDocLineToVisibleIndex[i] = static_cast( VisibleIndex::invalid ); + } } - mDocLineToVisibleIndex[visibleLine.line()] = - isFolded( visibleLine.line(), true ) ? static_cast( VisibleIndex::invalid ) - : visibleIdx; + if ( visibleLine.line() < (Int64)mDocLineToVisibleIndex.size() ) + mDocLineToVisibleIndex[visibleLine.line()] = + isFolded( visibleLine.line(), true ) ? static_cast( VisibleIndex::invalid ) + : visibleIdx; previousLineIdx = visibleLine.line(); } } @@ -448,7 +475,8 @@ void DocumentView::foldRegion( Int64 foldDocIdx ) { auto foldRegion = mDoc->getFoldRangeService().find( foldDocIdx ); if ( !foldRegion ) return; - if ( isOneToOne() && mDocLineToVisibleIndex.empty() ) + if ( isOneToOne() || mDocLineToVisibleIndex.empty() || mVisibleLines.empty() || + mVisibleLinesOffset.empty() ) invalidateCache(); Int64 toDocIdx = foldRegion->end().line(); changeVisibility( foldDocIdx + 1, toDocIdx, false ); @@ -467,6 +495,8 @@ void DocumentView::unfoldRegion( Int64 foldDocIdx, bool verifyConsistency, bool auto foldRegion = mDoc->getFoldRangeService().find( foldDocIdx ); if ( !foldRegion ) return; + if ( mDocLineToVisibleIndex.empty() || mVisibleLines.empty() || mVisibleLinesOffset.empty() ) + invalidateCache(); Int64 toDocIdx = foldRegion->end().line(); removeFoldedRegion( *foldRegion ); changeVisibility( foldDocIdx + 1, toDocIdx, true, recomputeOffset, @@ -559,7 +589,7 @@ void DocumentView::changeVisibility( Int64 fromDocIdx, Int64 toDocIdx, bool visi auto idxOffset = oldIdxFrom; for ( auto i = fromDocIdx; i <= toDocIdx; i++ ) { if ( isFolded( i, true ) ) { - if ( recomputeOffset ) { + if ( recomputeOffset && i < (Int64)mVisibleLinesOffset.size() ) { mVisibleLinesOffset[i] = LineWrap::computeOffsets( mDoc->line( i ).getText().view(), mFontStyle, mConfig.tabWidth, eemax( mMaxWidth - mWhiteSpaceWidth, mWhiteSpaceWidth ) ); @@ -571,11 +601,13 @@ void DocumentView::changeVisibility( Int64 fromDocIdx, Int64 toDocIdx, bool visi mConfig.keepIndentation, mConfig.tabWidth, mWhiteSpaceWidth, mConfig.tabStops ) : LineWrapInfo{ { 0 }, 0 }; - if ( recomputeOffset ) + if ( recomputeOffset && i < (Int64)mVisibleLinesOffset.size() ) mVisibleLinesOffset[i] = lb.paddingStart; for ( const auto& col : lb.wraps ) { - mVisibleLines.insert( mVisibleLines.begin() + idxOffset, { i, col } ); - idxOffset++; + if ( idxOffset >= 0 && idxOffset <= (Int64)mVisibleLines.size() ) { + mVisibleLines.insert( mVisibleLines.begin() + idxOffset, { i, col } ); + idxOffset++; + } } } @@ -585,17 +617,22 @@ void DocumentView::changeVisibility( Int64 fromDocIdx, Int64 toDocIdx, bool visi auto oldIdxToVI = toVisibleIndex( toDocIdx, true ); Int64 oldIdxFrom = static_cast( oldIdxFromVI ); Int64 oldIdxTo = static_cast( oldIdxToVI ); - if ( VisibleIndex::invalid == oldIdxFromVI || VisibleIndex::invalid == oldIdxToVI ) + if ( VisibleIndex::invalid == oldIdxFromVI || VisibleIndex::invalid == oldIdxToVI || + oldIdxFrom < 0 || oldIdxTo < 0 || + oldIdxFrom > oldIdxTo || oldIdxFrom >= (Int64)mVisibleLines.size() || + oldIdxTo >= (Int64)mVisibleLines.size() ) return; mVisibleLines.erase( mVisibleLines.begin() + oldIdxFrom, mVisibleLines.begin() + oldIdxTo + 1 ); for ( Int64 idx = fromDocIdx; idx <= toDocIdx; idx++ ) { - mDocLineToVisibleIndex[idx] = static_cast( VisibleIndex::invalid ); + if ( idx < (Int64)mDocLineToVisibleIndex.size() ) + mDocLineToVisibleIndex[idx] = static_cast( VisibleIndex::invalid ); } Int64 linesCount = mDoc->linesCount(); Int64 idxOffset = oldIdxTo - oldIdxFrom + 1; for ( Int64 idx = toDocIdx + 1; idx < linesCount; idx++ ) { - if ( mDocLineToVisibleIndex[idx] != static_cast( VisibleIndex::invalid ) ) + if ( idx < (Int64)mDocLineToVisibleIndex.size() && + mDocLineToVisibleIndex[idx] != static_cast( VisibleIndex::invalid ) ) mDocLineToVisibleIndex[idx] -= idxOffset; } } diff --git a/src/eepp/ui/doc/foldrangeservice.cpp b/src/eepp/ui/doc/foldrangeservice.cpp index 882a44053..2c809fa03 100644 --- a/src/eepp/ui/doc/foldrangeservice.cpp +++ b/src/eepp/ui/doc/foldrangeservice.cpp @@ -279,18 +279,54 @@ bool FoldRangeService::isFoldingRegionInLine( Int64 docIdx ) { } void FoldRangeService::shiftFoldingRegions( Int64 fromLine, Int64 numLines ) { - // TODO: Optimize this Lock l( mMutex ); FoldingRegions foldingRegions; - for ( auto& foldingRegion : mFoldingRegions ) { - if ( foldingRegion.second.start().line() > fromLine ) { - foldingRegion.second.start().setLine( foldingRegion.second.start().line() + numLines ); - foldingRegion.second.end().setLine( foldingRegion.second.end().line() + numLines ); - foldingRegions[foldingRegion.second.start().line()] = foldingRegion.second; + + if ( numLines < 0 ) { + Int64 removedLines = -numLines; + Int64 toLine = fromLine + removedLines; + + for ( auto& foldingRegion : mFoldingRegions ) { + auto& range = foldingRegion.second; + + if ( range.start().line() >= toLine ) { + range.start().setLine( range.start().line() + numLines ); + range.end().setLine( range.end().line() + numLines ); + foldingRegions[range.start().line()] = range; + } else if ( range.start().line() <= fromLine ) { + if ( range.end().line() >= toLine ) { + range.end().setLine( range.end().line() + numLines ); + foldingRegions[range.start().line()] = range; + } else if ( range.end().line() > fromLine ) { + range.end().setLine( fromLine ); + if ( range.start().line() < range.end().line() ) + foldingRegions[range.start().line()] = range; + } else { + foldingRegions[range.start().line()] = range; + } + } else { + if ( range.end().line() >= toLine ) { + range.start().setLine( fromLine ); + range.end().setLine( range.end().line() + numLines ); + if ( range.start().line() < range.end().line() ) + foldingRegions[range.start().line()] = range; + } + } + } + } else { + for ( auto& foldingRegion : mFoldingRegions ) { + auto& range = foldingRegion.second; + if ( range.start().line() >= fromLine ) { + range.start().setLine( range.start().line() + numLines ); + range.end().setLine( range.end().line() + numLines ); + } else if ( range.end().line() >= fromLine ) { + range.end().setLine( range.end().line() + numLines ); + } + foldingRegions[range.start().line()] = range; } - foldingRegions[foldingRegion.second.start().line()] = foldingRegion.second; } - mFoldingRegions = foldingRegions; + + mFoldingRegions = std::move( foldingRegions ); } void FoldRangeService::setFoldingRegions( std::vector regions ) { diff --git a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/objective-c.cpp b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/objective-c.cpp index 7d33821b6..fbe0dc638 100644 --- a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/objective-c.cpp +++ b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/objective-c.cpp @@ -232,6 +232,7 @@ SyntaxDefinition& addObjectiveC() { sd.setFoldRangeType( FoldRangeType::Braces ).setFoldBraces( { { '{', '}' } } ); sd.setBlockComment( { "/*", "*/" } ); + sd.addAlternativeName( "objc" ); return sd; } diff --git a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/objective-cpp.cpp b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/objective-cpp.cpp index aec0c5499..ae33ae7b5 100644 --- a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/objective-cpp.cpp +++ b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/objective-cpp.cpp @@ -235,6 +235,8 @@ SyntaxDefinition& addObjectiveCPP() { sd.setFoldRangeType( FoldRangeType::Braces ).setFoldBraces( { { '{', '}' } } ); sd.setBlockComment( { "/*", "*/" } ); + sd.addAlternativeName( "objective-cpp" ); + sd.addAlternativeName( "objcpp" ); return sd; } diff --git a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languagessyntaxhighlighting.cpp b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languagessyntaxhighlighting.cpp index 5f03e21df..f0c9dfc40 100644 --- a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languagessyntaxhighlighting.cpp +++ b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languagessyntaxhighlighting.cpp @@ -544,17 +544,18 @@ static void preDefinitionLangsChunk2( SyntaxDefinitionManager* sdm ) { sdm->addPreDefinition( { "Objeck", []() -> SyntaxDefinition& { return addObjeck(); }, { "%.obs$" } } ); - sdm->addPreDefinition( { - "Objective-C", - []() -> SyntaxDefinition& { return addObjectiveC(); }, - { "%.m$" }, - } ); + sdm->addPreDefinition( { "Objective-C", + []() -> SyntaxDefinition& { return addObjectiveC(); }, + { "%.m$" }, + {}, + { "objc" } } ); sdm->addPreDefinition( { "Objective-C++", []() -> SyntaxDefinition& { return addObjectiveCPP(); }, { "%.mm$" }, {}, - "objective-cpp" } ); + "objective-cpp", + { "objc++", "objcpp" } } ); sdm->addPreDefinition( { "OCaml", diff --git a/src/tests/unit_tests/uicodeeditor_test.cpp b/src/tests/unit_tests/uicodeeditor_test.cpp new file mode 100644 index 000000000..83cc8dd3e --- /dev/null +++ b/src/tests/unit_tests/uicodeeditor_test.cpp @@ -0,0 +1,311 @@ +#include "utest.h" +#include +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::UI; +using namespace EE::UI::Doc; +using namespace EE::Scene; + +static const std::string userCode = R"objcpp(#import "common.h" +#import +#import +#import +#import +#import + +@interface test () { + struct DrawSineWave { + struct AppState { + int width = 800; + int height = 600; + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + ULONG_PTR gdiplusToken; + }; + static void Draw(HDC hdc, int width, int height) { + + if (width <= 0 || height <= 0) + return; + + using namespace Gdiplus; + + Graphics graphics(hdc); + graphics.SetSmoothingMode(SmoothingModeAntiAlias); + + // Background + SolidBrush bgBrush(Color(255, 255, 255, 255)); + graphics.FillRectangle(&bgBrush, 0, 0, width, height); + + // Axis + Pen axisPen(Color(200, 200, 200), 1.0f); + graphics.DrawLine(&axisPen, 0, height / 2, width, height / 2); + + // Sine wave parameters + double amplitude = (height - 20) / 2.0; + double midY = height / 2.0; + double period = width; + double twoPi = 6.283185307179586; + + // Build points + std::vector pts; + double step = 0.25; // smaller step for smoother curve + for (double x = 0; x < width; x += step) { + double t = x / period; + double y = midY - amplitude * std::sin(twoPi * t); + pts.push_back(PointF(static_cast(x), static_cast(y))); + } + + // Draw sine wave + Pen sinePen(Color(0, 120, 215), 2.0f); + if (!pts.empty()) { + graphics.DrawLines(&sinePen, pts.data(), static_cast(pts.size())); + } + } +} +@end + +OF_APPLICATION_DELEGATE(test) + +@implementation test +- (void)applicationDidFinishLaunching:(OFNotification *)notification { + HINSTANCE hInstance = GetModuleHandle(nullptr); + DrawSineWave::Setup(hInstance, SW_SHOW); + [OFApplication terminate]; +} +@end +)objcpp"; + +#define VERIFY_CONSISTENCY( editor ) \ + { \ + const DocumentView& view = editor->getDocumentView(); \ + TextDocument& doc = editor->getDocument(); \ + if ( !view.isOneToOne() ) { \ + EXPECT_EQ( (size_t)doc.linesCount(), view.getDocLineToVisibleIndex().size() ); \ + EXPECT_EQ( (size_t)doc.linesCount(), view.getVisibleLinesOffset().size() ); \ + size_t expectedVisibleCount = 0; \ + for ( Int64 i = 0; i < (Int64)doc.linesCount(); i++ ) { \ + if ( view.isLineVisible( i ) ) { \ + EXPECT_NE( (Int64)VisibleIndex::invalid, (Int64)view.toVisibleIndex( i ) ); \ + Int64 startIdx = (Int64)view.toVisibleIndex( i ); \ + Int64 endIdx = (Int64)view.toVisibleIndex( i, true ); \ + expectedVisibleCount += ( endIdx - startIdx + 1 ); \ + } else { \ + EXPECT_EQ( (Int64)VisibleIndex::invalid, (Int64)view.toVisibleIndex( i ) ); \ + } \ + } \ + EXPECT_EQ( expectedVisibleCount, view.getVisibleLinesCount() ); \ + } \ + } + +UTEST( UICodeEditor, DocumentViewStressTest ) { + UIApplication app( + WindowSettings( 800, 600, "eepp - Stress Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash() ) ); + + auto* editor = UICodeEditor::New(); + editor->setParent( (Node*)app.getUI() ); + editor->setPixelsSize( 800, 600 ); + editor->getDocument().setSyntaxDefinition( + SyntaxDefinitionManager::instance()->getByLanguageName( "C++" ) ); + + auto resetEditor = [&]() { + editor->getDocument().selectAll(); + editor->getDocument().deleteSelection(); + editor->getDocument().textInput( userCode ); + editor->getDocument().getFoldRangeService().findRegionsNative(); + editor->unfoldAll(); + }; + + // --- SCENARIO 1: Folding Only (No Wrap) --- + editor->setLineWrapMode( LineWrapMode::NoWrap ); + resetEditor(); + + // Fold everything + editor->foldAll(); + VERIFY_CONSISTENCY( editor ); + + // Delete while folded (should unfold affected) + editor->getDocument().setSelection( { { 2, 0 }, { 10, 0 } } ); + editor->getDocument().deleteSelection(); + VERIFY_CONSISTENCY( editor ); + editor->getDocument().undo(); + VERIFY_CONSISTENCY( editor ); + + // Insert text in the middle of a folded region + editor->foldAll(); + editor->getDocument().setSelection( { { 5, 5 }, { 5, 5 } } ); + editor->getDocument().textInput( "STRESS_TEST" ); // Should unfold line 5 + VERIFY_CONSISTENCY( editor ); + + // --- SCENARIO 2: Wrapping Only (No Folds) --- + resetEditor(); + editor->setLineWrapMode( LineWrapMode::Letter ); + editor->setPixelsSize( 100, 600 ); // Force lots of wraps + VERIFY_CONSISTENCY( editor ); + + // Multi-line delete with wraps + editor->getDocument().setSelection( { { 1, 5 }, { 4, 2 } } ); + editor->getDocument().deleteSelection(); + VERIFY_CONSISTENCY( editor ); + editor->getDocument().undo(); + VERIFY_CONSISTENCY( editor ); + + // --- SCENARIO 3: Folding + Wrapping --- + resetEditor(); + editor->setLineWrapMode( LineWrapMode::Letter ); + editor->setPixelsSize( 100, 600 ); + editor->foldAll(); + VERIFY_CONSISTENCY( editor ); + + // Delete range straddling multiple folded regions with wraps + // userCode has folds starting at lines: 1, 2, 3, 7 + editor->getDocument().setSelection( { { 0, 0 }, { 12, 0 } } ); + editor->getDocument().deleteSelection(); + VERIFY_CONSISTENCY( editor ); + editor->getDocument().undo(); + VERIFY_CONSISTENCY( editor ); + + // Random heavy operations + resetEditor(); + editor->setLineWrapMode( LineWrapMode::Word ); + editor->setPixelsSize( 200, 600 ); + + for ( int i = 0; i < 5; i++ ) { + editor->foldAll(); + editor->getDocument().setSelection( { { i * 2, 0 }, { i * 2 + 1, 5 } } ); + editor->getDocument().textInput( "RANDOM_INSERTION\nMORE_LINES\n" ); + VERIFY_CONSISTENCY( editor ); + editor->unfoldAll(); + VERIFY_CONSISTENCY( editor ); + } + + // Final check: Delete everything + editor->getDocument().selectAll(); + editor->getDocument().deleteSelection(); + VERIFY_CONSISTENCY( editor ); + editor->getDocument().undo(); + VERIFY_CONSISTENCY( editor ); +} + +UTEST( UICodeEditor, FoldingCrashReproduction ) { + UIApplication app( + WindowSettings( 800, 600, "eepp - Reproduce Crash", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash() ) ); + + auto* editor = UICodeEditor::New(); + editor->setParent( (Node*)app.getUI() ); + editor->setPixelsSize( 800, 600 ); + + editor->getDocument().setSyntaxDefinition( + SyntaxDefinitionManager::instance()->getByLanguageName( "C++" ) ); + editor->getDocument().textInput( userCode ); + + // Wait for folding regions to be updated + editor->getDocument().getFoldRangeService().findRegionsNative(); + + // Try to reproduce the sequence that crashed: + // 1. Fold regions + editor->foldAll(); + + // 2. Select everything and delete + editor->getDocument().selectAll(); + editor->getDocument().deleteSelection(); + + // 3. Undo + editor->getDocument().undo(); + + // 4. Unfold all + editor->unfoldAll(); + + // Another sequence: select range straddling folded region and delete + editor->getDocument().textInput( userCode ); + editor->getDocument().getFoldRangeService().findRegionsNative(); + auto regions = editor->getDocument().getFoldRangeService().getFoldingRegions(); + + if ( !regions.empty() ) { + auto firstRegionLine = regions.begin()->first; + auto firstRegionRange = regions.begin()->second; + + editor->fold( firstRegionLine ); + + // Select from before the folded region to after + TextRange sel( { firstRegionLine, 0 }, { firstRegionRange.end().line() + 1, 0 } ); + editor->getDocument().setSelection( sel ); + editor->getDocument().deleteSelection(); + editor->getDocument().undo(); + } +} + +UTEST( UICodeEditor, ReproduceFoldingCrash ) { + UIApplication app( + WindowSettings( 800, 600, "eepp - Reproduce Crash", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash() ) ); + + auto* editor = UICodeEditor::New(); + editor->setParent( (Node*)app.getUI() ); + editor->setPixelsSize( 800, 600 ); + + auto languages = SyntaxDefinitionManager::instance()->getLanguageNames(); + editor->getDocument().setSyntaxDefinition( + SyntaxDefinitionManager::instance()->getByLanguageName( "C++" ) ); + editor->getDocument().textInput( userCode ); + + // Wait for folding regions to be updated + editor->getDocument().getFoldRangeService().findRegionsNative(); + + auto regions = editor->getDocument().getFoldRangeService().getFoldingRegions(); + + // Brute force: Try all combinations of folded regions + size_t numRegions = regions.size(); + if ( numRegions > 8 ) + numRegions = 8; // Limit for speed + + for ( size_t i = 0; i < ( (size_t)1 << numRegions ); ++i ) { + editor->getDocument().resetUndoRedo(); + editor->getDocument().resetSelection(); + editor->unfoldAll(); + + size_t idx = 0; + for ( auto const& [line, range] : regions ) { + if ( ( i >> idx ) & 1 ) { + editor->fold( line ); + } + if ( ++idx >= numRegions ) + break; + } + + // Try various selections and deletions + idx = 0; + for ( auto const& [line, range] : regions ) { + // Selection from before the folded region to after + TextRange sel( { line, 0 }, { range.end().line() + 1, 0 } ); + editor->getDocument().setSelection( sel ); + editor->getDocument().deleteSelection(); + editor->getDocument().undo(); + + // Selection starting inside the folded region + if ( range.end().line() > line ) { + TextRange sel2( { line + 1, 0 }, { range.end().line() + 1, 0 } ); + editor->getDocument().setSelection( sel2 ); + editor->getDocument().deleteSelection(); + editor->getDocument().undo(); + } + + if ( ++idx >= numRegions ) + break; + } + + // Also try foldAll then select everything and delete + editor->foldAll(); + editor->getDocument().selectAll(); + editor->getDocument().deleteSelection(); + editor->getDocument().undo(); + } +}