Fixes and sanity checks for issue SpartanJ/ecode#836. Added several tests that triggered the crashes.

This commit is contained in:
Martín Lucas Golini
2026-03-06 19:14:38 -03:00
parent 4edc8453fd
commit cebde979c9
8 changed files with 420 additions and 30 deletions

View File

@@ -33,7 +33,6 @@
} \
\
T* T::existsSingleton() { \
Lock l( ms_mutex ); \
return ms_singleton; \
} \
\
@@ -62,7 +61,6 @@ template <typename T> class Singleton {
public:
/** Get the singleton pointer (without instance verification) */
static T* existsSingleton() {
Lock l( ms_mutex );
return ms_singleton;
}

View File

@@ -139,6 +139,10 @@ class EE_API DocumentView {
bool usesTabStops() const { return mConfig.tabStops; }
const std::vector<Int64> getDocLineToVisibleIndex() const { return mDocLineToVisibleIndex; }
const std::vector<Float> getVisibleLinesOffset() const { return mVisibleLinesOffset; }
protected:
std::shared_ptr<TextDocument> mDoc;
FontStyleConfig mFontStyle;

View File

@@ -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<Int64>( toVisibleIndex( fromLine, false ) );
Int64 oldIdxTo = static_cast<Int64>( toVisibleIndex( toLine, true ) );
if ( oldIdxFrom == static_cast<Int64>( VisibleIndex::invalid ) ||
oldIdxTo == static_cast<Int64>( 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<Int64>( 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<Int64>( VisibleIndex::invalid );
for ( Int64 i = previousLineIdx + 1; i < visibleLine.line(); i++ ) {
if ( i < (Int64)mDocLineToVisibleIndex.size() )
mDocLineToVisibleIndex[i] = static_cast<Int64>( VisibleIndex::invalid );
}
}
mDocLineToVisibleIndex[visibleLine.line()] =
isFolded( visibleLine.line(), true ) ? static_cast<Int64>( VisibleIndex::invalid )
: visibleIdx;
if ( visibleLine.line() < (Int64)mDocLineToVisibleIndex.size() )
mDocLineToVisibleIndex[visibleLine.line()] =
isFolded( visibleLine.line(), true ) ? static_cast<Int64>( 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<Int64>( oldIdxFromVI );
Int64 oldIdxTo = static_cast<Int64>( 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<Int64>( VisibleIndex::invalid );
if ( idx < (Int64)mDocLineToVisibleIndex.size() )
mDocLineToVisibleIndex[idx] = static_cast<Int64>( VisibleIndex::invalid );
}
Int64 linesCount = mDoc->linesCount();
Int64 idxOffset = oldIdxTo - oldIdxFrom + 1;
for ( Int64 idx = toDocIdx + 1; idx < linesCount; idx++ ) {
if ( mDocLineToVisibleIndex[idx] != static_cast<Int64>( VisibleIndex::invalid ) )
if ( idx < (Int64)mDocLineToVisibleIndex.size() &&
mDocLineToVisibleIndex[idx] != static_cast<Int64>( VisibleIndex::invalid ) )
mDocLineToVisibleIndex[idx] -= idxOffset;
}
}

View File

@@ -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<TextRange> regions ) {

View File

@@ -232,6 +232,7 @@ SyntaxDefinition& addObjectiveC() {
sd.setFoldRangeType( FoldRangeType::Braces ).setFoldBraces( { { '{', '}' } } );
sd.setBlockComment( { "/*", "*/" } );
sd.addAlternativeName( "objc" );
return sd;
}

View File

@@ -235,6 +235,8 @@ SyntaxDefinition& addObjectiveCPP() {
sd.setFoldRangeType( FoldRangeType::Braces ).setFoldBraces( { { '{', '}' } } );
sd.setBlockComment( { "/*", "*/" } );
sd.addAlternativeName( "objective-cpp" );
sd.addAlternativeName( "objcpp" );
return sd;
}

View File

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

View File

@@ -0,0 +1,311 @@
#include "utest.h"
#include <eepp/scene/node.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/ui/doc/syntaxdefinitionmanager.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uicodeeditor.hpp>
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 <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( 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();
}
}