#include "utest.hpp" #include #include #include #include #include #include #include #include #include #include "../../tools/ecode/keybindingshelper.cpp" using namespace EE; using namespace EE::UI; using namespace EE::UI::Doc; using namespace EE::Scene; using namespace EE::System; class TestableCodeEditor : public UICodeEditor { public: TestableCodeEditor() : UICodeEditor() {} bool isLongestLineWidthDirtyForTest() const { return mLongestLineWidthDirty; } void clearLongestLineWidthDirtyForTest() { mLongestLineWidthDirty = false; } TextRanges getHighlightWordCacheForTest() { Lock l( mHighlightWordCacheMutex ); return mHighlightWordCache; } void clearHighlightWordCacheForTest() { Lock l( mHighlightWordCacheMutex ); mHighlightWordCache.clear(); } }; template static bool waitForCondition( Predicate&& predicate, const Time& timeout = Seconds( 10 ) ) { Clock clock; while ( !predicate() && clock.getElapsedTime() < timeout ) Sys::sleep( Milliseconds( 1 ) ); return predicate(); } static bool waitForThreadPool( const std::shared_ptr& threadPool ) { auto completed = std::make_shared( false ); threadPool->run( [completed] { completed->store( true, std::memory_order_release ); } ); return waitForCondition( [completed] { return completed->load( std::memory_order_acquire ); } ); } static void dispatchDebouncedEditorWork() { Sys::sleep( Milliseconds( 20 ) ); SceneManager::instance()->update(); } static String makeLargeMarkdownDocument() { constexpr size_t blockCount = 16384; String text; text.reserve( blockCount * 80 ); for ( size_t i = 0; i < blockCount; ++i ) { text.append( "## Heading\nInline `code` and ``span``.\n```cpp\nvalue\n```\n" ); } text.append( "unique-final-token\n" ); return text; } UTEST( SyntaxColorScheme, CopiesShareStorageAndDetachOnMutation ) { auto defaults = SyntaxColorScheme::getDefaultDark(); auto copy = defaults; EXPECT_TRUE( &defaults.getSyntaxStyle( SyntaxStyleTypes::Keyword ) == ©.getSyntaxStyle( SyntaxStyleTypes::Keyword ) ); const auto defaultKeyword = defaults.getSyntaxStyle( SyntaxStyleTypes::Keyword ).color; copy.setSyntaxStyle( SyntaxStyleTypes::Keyword, SyntaxColorScheme::Style{ Color::Red } ); EXPECT_TRUE( copy.getSyntaxStyle( SyntaxStyleTypes::Keyword ).color == Color::Red ); EXPECT_TRUE( defaults.getSyntaxStyle( SyntaxStyleTypes::Keyword ).color == defaultKeyword ); EXPECT_TRUE( &defaults.getSyntaxStyle( SyntaxStyleTypes::Keyword ) != ©.getSyntaxStyle( SyntaxStyleTypes::Keyword ) ); } UTEST( SyntaxDefinitionManager, ManyLanguageExtensionCacheInvalidatesOnAdd ) { auto* manager = SyntaxDefinitionManager::instance(); const std::string extension( ".eepp-many-languages-cache-test" ); const std::string preDefinitionExtension( ".eepp-many-languages-predefinition-cache-test" ); EXPECT_FALSE( manager->extensionCanRepresentManyLanguages( extension ) ); manager->add( { "EEPP Cache Test A", { extension }, {} } ); EXPECT_FALSE( manager->extensionCanRepresentManyLanguages( extension ) ); manager->add( { "EEPP Cache Test B", { extension }, {} } ); EXPECT_TRUE( manager->extensionCanRepresentManyLanguages( extension ) ); EXPECT_TRUE( manager->extensionCanRepresentManyLanguages( extension ) ); manager->add( { "EEPP Cache Test C", { preDefinitionExtension }, {} } ); EXPECT_FALSE( manager->extensionCanRepresentManyLanguages( preDefinitionExtension ) ); manager->addPreDefinition( { "EEPP Cache Test PreDefinition", []() -> SyntaxDefinition& { return SyntaxDefinitionManager::instance()->add( { "EEPP Cache Test PreDefinition", {}, {} } ); }, { preDefinitionExtension } } ); EXPECT_TRUE( manager->extensionCanRepresentManyLanguages( preDefinitionExtension ) ); } UTEST( MainThreadLifetime, InvalidatedCallbacksDoNotRun ) { UIApplication app( WindowSettings{ 320, 240, "eepp - main thread lifetime test" } ); int owner = 42; MainThreadLifetime lifetime( &owner, app.getUI() ); auto weak = lifetime.weakHandle(); bool called = false; weak.run( [&called]( int* ) { called = true; } ); lifetime.invalidate(); SceneManager::instance()->update(); EXPECT_FALSE( called ); } UTEST( MainThreadLifetime, LiveCallbacksReceiveOwner ) { UIApplication app( WindowSettings{ 320, 240, "eepp - main thread lifetime test" } ); int owner = 42; MainThreadLifetime lifetime( &owner, app.getUI() ); int value = 0; lifetime.weakHandle().run( [&value]( int* object ) { value = *object; } ); SceneManager::instance()->update(); EXPECT_EQ( 42, value ); } UTEST( MainThreadLifetime, DispatcherCanBeAttachedAfterConstruction ) { UIApplication app( WindowSettings{ 320, 240, "eepp - deferred dispatcher test" } ); int owner = 42; MainThreadLifetime lifetime( &owner, nullptr ); bool called = false; lifetime.weakHandle().run( [&called]( int* ) { called = true; } ); SceneManager::instance()->update(); EXPECT_FALSE( called ); lifetime.setDispatcher( app.getUI() ); lifetime.weakHandle().run( [&called]( int* ) { called = true; } ); SceneManager::instance()->update(); EXPECT_TRUE( called ); } UTEST( UICodeEditor, DefersLongestLineMeasurementForLargeChanges ) { UIApplication app( WindowSettings{ 320, 240, "eepp - deferred longest line test" } ); auto* editor = eeNew( TestableCodeEditor, () ); editor->setPixelsSize( 160, 80 ); editor->setParent( app.getUI()->getRoot() ); editor->setFindLongestLineWidthUpdateFrequency( Time::Zero ); app.getUI()->flushDirtyStyleAndLayout(); editor->setLineWrapMode( LineWrapMode::NoWrap ); EXPECT_EQ( LineWrapMode::NoWrap, editor->getLineWrapMode() ); editor->clearLongestLineWidthDirtyForTest(); String text; for ( size_t i = 0; i < 64; ++i ) text += i == 32 ? String( 256, 'x' ) + "\n" : "short\n"; editor->getDocument().textInput( text ); EXPECT_TRUE( editor->isLongestLineWidthDirtyForTest() ); editor->clearLongestLineWidthDirtyForTest(); editor->getDocument().insert( 0, { 0, 0 }, "!" ); EXPECT_FALSE( editor->isLongestLineWidthDirtyForTest() ); editor->getDocument().reset(); editor->clearLongestLineWidthDirtyForTest(); editor->getDocument().textInput( String( 4097, 'x' ) ); EXPECT_TRUE( editor->isLongestLineWidthDirtyForTest() ); editor->clearLongestLineWidthDirtyForTest(); editor->getDocument().insert( 0, { 0, 0 }, "!" ); EXPECT_TRUE( editor->isLongestLineWidthDirtyForTest() ); eeDelete( editor ); } UTEST( UICodeEditor, AsyncHighlightRejectsStaleResultsAfterLargePaste ) { UIApplication app( WindowSettings{ 320, 240, "eepp - async highlight test" } ); auto threadPool = ThreadPool::createShared( 1 ); app.getUI()->setThreadPool( threadPool ); auto* editor = eeNew( TestableCodeEditor, () ); editor->setParent( app.getUI()->getRoot() ); editor->getDocument().textInput( makeLargeMarkdownDocument() ); editor->clearHighlightWordCacheForTest(); editor->setHighlightWord( { "`" } ); dispatchDebouncedEditorWork(); ASSERT_TRUE( waitForThreadPool( threadPool ) ); // The old result is already queued for the main thread. Changing the query before pumping that // callback must make the result stale and leave the cache untouched. editor->setHighlightWord( { "unique-final-token" } ); SceneManager::instance()->update(); EXPECT_TRUE( editor->getHighlightWordCacheForTest().empty() ); dispatchDebouncedEditorWork(); ASSERT_TRUE( waitForThreadPool( threadPool ) ); SceneManager::instance()->update(); auto ranges = editor->getHighlightWordCacheForTest(); ASSERT_EQ( size_t{ 1 }, ranges.size() ); // Reproduce the #956 workload: a fresh multiline paste followed by rapid literal backtick // queries while an earlier highlight scan can still be publishing its result. for ( size_t i = 0; i < 4; ++i ) { editor->setHighlightWord( { "`" } ); dispatchDebouncedEditorWork(); editor->setHighlightWord( { "``" } ); editor->setHighlightWord( { "```" } ); dispatchDebouncedEditorWork(); ASSERT_TRUE( waitForThreadPool( threadPool ) ); SceneManager::instance()->update(); } EXPECT_STDSTREQ( "```", editor->getHighlightWord().text.toUtf8() ); auto expected = editor->getDocument().findAll( "```" ).ranges(); EXPECT_TRUE( expected == editor->getHighlightWordCacheForTest() ); eeDelete( editor ); app.getUI()->setThreadPool( nullptr ); threadPool.reset(); } UTEST( UICodeEditor, AsyncHighlightSurvivesEditorDestruction ) { UIApplication app( WindowSettings{ 320, 240, "eepp - async highlight destruction test" } ); auto threadPool = ThreadPool::createShared( 1 ); app.getUI()->setThreadPool( threadPool ); std::atomic_bool blockerStarted{ false }; std::atomic_bool releaseBlocker{ false }; threadPool->run( [&] { blockerStarted.store( true, std::memory_order_release ); while ( !releaseBlocker.load( std::memory_order_acquire ) ) Sys::sleep( Milliseconds( 1 ) ); } ); const bool started = waitForCondition( [&] { return blockerStarted.load( std::memory_order_acquire ); } ); if ( !started ) releaseBlocker.store( true, std::memory_order_release ); ASSERT_TRUE( started ); auto* editor = eeNew( TestableCodeEditor, () ); editor->setParent( app.getUI()->getRoot() ); editor->getDocument().textInput( makeLargeMarkdownDocument() ); editor->setHighlightWord( { "missing-highlight-value" } ); dispatchDebouncedEditorWork(); const Uint64 tag = reinterpret_cast( editor ); const bool searchQueued = threadPool->existsTagInQueue( tag ); releaseBlocker.store( true, std::memory_order_release ); ASSERT_TRUE( searchQueued ); ASSERT_TRUE( waitForCondition( [&] { return !threadPool->existsTagInQueue( tag ); } ) ); eeDelete( editor ); app.getUI()->setThreadPool( nullptr ); threadPool.reset(); SceneManager::instance()->update(); } UTEST( UICodeEditor, DefaultKeybindingCacheTracksConfiguredModifiers ) { const Uint32 originalDefaultModifier = KeyMod::getDefaultModifier(); const Uint32 originalSecondaryModifier = KeyMod::getDefaultSecondaryModifier(); auto defaultBindings = UICodeEditor::getDefaultKeybindings(); const auto& defaultShortcutMap = defaultBindings->getShortcutMap(); auto copy = defaultShortcutMap.find( KeyBindings::Shortcut{ KEY_C, originalDefaultModifier } ); EXPECT_TRUE( copy != defaultShortcutMap.end() ); if ( copy != defaultShortcutMap.end() ) EXPECT_STREQ( "copy", copy->second.c_str() ); KeyMod::setDefaultModifier( KEYMOD_LALT ); KeyMod::setDefaultSecondaryModifier( KEYMOD_META ); auto reconfiguredBindings = UICodeEditor::getDefaultKeybindings(); const auto& reconfiguredShortcutMap = reconfiguredBindings->getShortcutMap(); copy = reconfiguredShortcutMap.find( KeyBindings::Shortcut{ KEY_C, KEYMOD_LALT } ); EXPECT_TRUE( copy != reconfiguredShortcutMap.end() ); if ( copy != reconfiguredShortcutMap.end() ) EXPECT_STREQ( "copy", copy->second.c_str() ); KeyMod::setDefaultModifier( originalDefaultModifier ); KeyMod::setDefaultSecondaryModifier( originalSecondaryModifier ); auto restoredBindings = UICodeEditor::getDefaultKeybindings(); const auto& restoredShortcutMap = restoredBindings->getShortcutMap(); copy = restoredShortcutMap.find( KeyBindings::Shortcut{ KEY_C, originalDefaultModifier } ); EXPECT_TRUE( copy != restoredShortcutMap.end() ); if ( copy != restoredShortcutMap.end() ) EXPECT_STREQ( "copy", copy->second.c_str() ); } 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( KeybindingsHelper, PreservesUserShortcutWhenAddingBinding ) { UIApplication app( WindowSettings( 320, 240, "eepp - KeybindingsHelper Test", WindowStyle::Default, WindowBackend::Default, 32 ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); const std::string path = Sys::getTempPath() + "eepp_keybindingshelper.cfg"; const std::string statePath = Sys::getTempPath() + "eepp_keybindingshelper_state.cfg"; FileSystem::fileRemove( path ); FileSystem::fileRemove( statePath ); IniFile ini( path, false ); IniFile iniState( statePath, false ); const std::string group( "editor" ); const std::string modD( "mod+d" ); const std::string modX( "mod+x" ); const std::string modE( "mod+e" ); ini.setValue( group, modD, std::string( "duplicate-line-or-selection" ) ); ini.setValue( group, modX, std::string( "cut" ) ); ini.setValue( group, modE, std::string( "show-markdown-preview" ) ); std::unordered_map keybindings; std::unordered_map invertedKeybindings; const KeyBindings::ShortcutMap defaultKeybindings{ { { KEY_D, KeyMod::getDefaultModifier() }, "select-word" }, { { KEY_X, KeyMod::getDefaultModifier() }, "cut" }, }; ecode::KeybindingsHelper::updateKeybindings( ini, group, app.getWindow()->getInput(), keybindings, invertedKeybindings, defaultKeybindings, false, {}, iniState ); ASSERT_TRUE( keybindings.find( modD ) != keybindings.end() ); const std::string savedModD = ini.getValue( group, modD, "" ); EXPECT_STREQ( keybindings[modD].c_str(), "duplicate-line-or-selection" ); EXPECT_STREQ( savedModD.c_str(), "duplicate-line-or-selection" ); EXPECT_STREQ( invertedKeybindings["duplicate-line-or-selection"].c_str(), modD.c_str() ); FileSystem::fileRemove( path ); FileSystem::fileRemove( statePath ); } UTEST( KeybindingsHelper, RestoresMissingCommandWhenShortcutIsFree ) { UIApplication app( WindowSettings( 320, 240, "eepp - KeybindingsHelper Test", WindowStyle::Default, WindowBackend::Default, 32 ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); const std::string path = Sys::getTempPath() + "eepp_keybindingshelper_restore.cfg"; const std::string statePath = Sys::getTempPath() + "eepp_keybindingshelper_restore_state.cfg"; FileSystem::fileRemove( path ); FileSystem::fileRemove( statePath ); IniFile ini( path, false ); IniFile iniState( statePath, false ); const std::string group( "editor" ); const std::string modD( "mod+d" ); const std::string modX( "mod+x" ); ini.setValue( group, modX, std::string( "cut" ) ); std::unordered_map keybindings; std::unordered_map invertedKeybindings; const KeyBindings::ShortcutMap defaultKeybindings{ { { KEY_D, KeyMod::getDefaultModifier() }, "select-word" }, { { KEY_X, KeyMod::getDefaultModifier() }, "cut" }, }; ecode::KeybindingsHelper::updateKeybindings( ini, group, app.getWindow()->getInput(), keybindings, invertedKeybindings, defaultKeybindings, false, {}, iniState ); ASSERT_TRUE( keybindings.find( modD ) != keybindings.end() ); const std::string savedModD = ini.getValue( group, modD, "" ); EXPECT_STREQ( keybindings[modD].c_str(), "select-word" ); EXPECT_STREQ( savedModD.c_str(), "select-word" ); EXPECT_STREQ( invertedKeybindings["select-word"].c_str(), modD.c_str() ); FileSystem::fileRemove( path ); FileSystem::fileRemove( statePath ); } UTEST( UICodeEditor, DocumentViewStressTest ) { UIApplication app( WindowSettings( 800, 600, "eepp - Stress Test", WindowStyle::Default, WindowBackend::Default, 32 ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); 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(); } }