mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-09-22 13:01:05 +03:00
Snapshot document search state before dispatching highlight workers and retain the searched document for the duration of each job. Publish results through MainThreadLifetime and reject results when the editor has changed document or search parameters. Remove the obsolete worker counter and destructor wait used for editor lifetime management. Use atomic cancellation flags in TextDocument::findAll() and protect active-search map inspection during destruction. Add regression coverage for concurrent cancellation, stale highlight results, rapid backtick searches after a large multiline paste, and editor destruction during active search. Refs SpartanJ/ecode#956.
667 lines
25 KiB
C++
667 lines
25 KiB
C++
#include "utest.hpp"
|
|
#include <atomic>
|
|
#include <eepp/scene/node.hpp>
|
|
#include <eepp/scene/scenemanager.hpp>
|
|
#include <eepp/system/filesystem.hpp>
|
|
#include <eepp/system/inifile.hpp>
|
|
#include <eepp/ui/doc/syntaxdefinitionmanager.hpp>
|
|
#include <eepp/ui/uiapplication.hpp>
|
|
#include <eepp/ui/uicodeeditor.hpp>
|
|
#include <eepp/ui/uiscenenode.hpp>
|
|
|
|
#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 <typename Predicate>
|
|
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>& threadPool ) {
|
|
auto completed = std::make_shared<std::atomic_bool>( 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<int> 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<int> 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<int> 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<Uint64>( 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 <cmath>
|
|
#import <gdiplus.h>
|
|
#import <iostream>
|
|
#import <vector>
|
|
#import <windows.h>
|
|
|
|
@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<PointF> 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<REAL>(x), static_cast<REAL>(y)));
|
|
}
|
|
|
|
// Draw sine wave
|
|
Pen sinePen(Color(0, 120, 215), 2.0f);
|
|
if (!pts.empty()) {
|
|
graphics.DrawLines(&sinePen, pts.data(), static_cast<INT>(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<std::string, std::string> keybindings;
|
|
std::unordered_map<std::string, std::string> 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<std::string, std::string> keybindings;
|
|
std::unordered_map<std::string, std::string> 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();
|
|
}
|
|
}
|