Improve asynchronous filesystem event processing

- queue and batch filesystem model updates on the main thread
  - process directory-tree updates and worker-affine listeners asynchronously
  - coalesce adjacent modified events for the same path
  - preserve captured file metadata across delayed event delivery
  - add event, path, and thread-affinity filters for listener subscriptions
  - configure optimized per-plugin filesystem subscriptions
  - make listener removal wait for active callbacks safely
  - handle self-removal without deadlocking
  - support allocation-free matching of moved source paths
  - synchronize project directory-tree mutations with active searches
  - centralize diff-view button visibility around find/replace state
  - improve code editor wrap-content and scrollbar sizing
  - add queue, listener, and filesystem-model regression tests
This commit is contained in:
Martín Lucas Golini
2026-08-21 01:32:36 -03:00
parent c13710dddc
commit 864b2a9b3f
25 changed files with 860 additions and 109 deletions
+2
View File
@@ -132,6 +132,8 @@ class EE_API Event {
OnFocusWithinLoss, OnFocusWithinLoss,
OnItemsCountChange, OnItemsCountChange,
OnApply, OnApply,
OnShowFindReplace,
OnHideFindReplace,
NoEvent = eeINDEX_NOT_FOUND NoEvent = eeINDEX_NOT_FOUND
}; };
+11 -1
View File
@@ -224,6 +224,14 @@ class EE_API FileSystemModel : public Model {
*/ */
bool handleFileEvent( const FileEvent& event ); bool handleFileEvent( const FileEvent& event );
/**
* @brief Processes a filesystem event using metadata captured when the event was received.
*
* This avoids querying the filesystem from the main thread and preserves the state observed by
* an asynchronous listener even if a later event changes the path before this event is applied.
*/
bool handleFileEvent( const FileEvent& event, const FileInfo& file );
virtual bool isValid( const ModelIndex& index ) const override; virtual bool isValid( const ModelIndex& index ) const override;
virtual bool classModelRoleEnabled() { return true; } virtual bool classModelRoleEnabled() { return true; }
@@ -256,7 +264,9 @@ class EE_API FileSystemModel : public Model {
size_t getFileIndex( Node* parent, const FileInfo& file, const Node* excludedNode = nullptr ); size_t getFileIndex( Node* parent, const FileInfo& file, const Node* excludedNode = nullptr );
bool handleFileEventLocked( const FileEvent& event ); bool handleFileEventLocked( const FileEvent& event, const FileInfo* preparedFile = nullptr );
bool handleFileEvent( const FileEvent& event, const FileInfo* preparedFile );
void setupColumnNames( Translator* translator ); void setupColumnNames( Translator* translator );
}; };
+1
View File
@@ -135,6 +135,7 @@ class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter {
void computeSubLineDiff( DiffLine& oldLine, DiffLine& newLine ); void computeSubLineDiff( DiffLine& oldLine, DiffLine& newLine );
void updateEditorsText(); void updateEditorsText();
void updateButtonsText(); void updateButtonsText();
void updateButtonsVisibility();
void createImageViewers(); void createImageViewers();
bool loadImageDiffFromPaths( const std::string& oldFilePath, const std::string& newFilePath ); bool loadImageDiffFromPaths( const std::string& oldFilePath, const std::string& newFilePath );
void updateImageDiffView(); void updateImageDiffView();
+2
View File
@@ -869,6 +869,8 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client {
void setDynamicTheming( bool set ); void setDynamicTheming( bool set );
const Tools::UIDocFindReplace* getFindReplace() const { return mFindReplace; }
protected: protected:
struct LastXOffset { struct LastXOffset {
TextPosition position{ 0, 0 }; TextPosition position{ 0, 0 };
+29 -13
View File
@@ -764,12 +764,14 @@ size_t FileSystemModel::getFileIndex( Node* parent, const FileInfo& file,
return pos; return pos;
} }
bool FileSystemModel::handleFileEventLocked( const FileEvent& event ) { bool FileSystemModel::handleFileEventLocked( const FileEvent& event,
const FileInfo* preparedFile ) {
switch ( event.type ) { switch ( event.type ) {
case FileSystemEventType::Add: { case FileSystemEventType::Add: {
FileInfo file( event.directory + event.filename, false ); FileInfo file =
preparedFile ? *preparedFile : FileInfo( event.directory + event.filename, false );
if ( !file.exists() ) if ( !preparedFile && !file.exists() )
return false; return false;
if ( ( getMode() == Mode::DirectoriesOnly && !file.isDirectory() ) || if ( ( getMode() == Mode::DirectoriesOnly && !file.isDirectory() ) ||
@@ -789,11 +791,6 @@ bool FileSystemModel::handleFileEventLocked( const FileEvent& event ) {
if ( childNodeExists ) if ( childNodeExists )
return false; return false;
Node* childNode = parent->createChild( file.getFileName(), *this );
if ( childNode == nullptr || childNode->getName().empty() )
return false;
size_t pos = getFileIndex( parent, file ); size_t pos = getFileIndex( parent, file );
const auto& displayCfg = getDisplayConfig(); const auto& displayCfg = getDisplayConfig();
@@ -804,6 +801,15 @@ bool FileSystemModel::handleFileEventLocked( const FileEvent& event ) {
if ( pos == INDEX_ALREADY_EXISTS ) if ( pos == INDEX_ALREADY_EXISTS )
return false; return false;
// The listener can provide metadata captured on its worker thread. Construct the node
// from that snapshot instead of probing the path again on the UI thread.
Node* childNode = eeNew( Node, ( FileInfo( file ), parent, *this ) );
if ( childNode == nullptr || childNode->getName().empty() ) {
eeDelete( childNode );
return false;
}
beginInsertRows( parent->index( *this, 0 ), pos, pos ); beginInsertRows( parent->index( *this, 0 ), pos, pos );
{ {
@@ -846,7 +852,8 @@ bool FileSystemModel::handleFileEventLocked( const FileEvent& event ) {
break; break;
} }
case FileSystemEventType::Delete: { case FileSystemEventType::Delete: {
FileInfo file( event.directory + event.filename, false ); FileInfo file =
preparedFile ? *preparedFile : FileInfo( event.directory + event.filename, false );
auto* child = getNodeFromPath( file.getFilepath(), file.isDirectory(), false ); auto* child = getNodeFromPath( file.getFilepath(), file.isDirectory(), false );
if ( !child ) if ( !child )
@@ -922,18 +929,19 @@ bool FileSystemModel::handleFileEventLocked( const FileEvent& event ) {
break; break;
} }
case FileSystemEventType::Moved: { case FileSystemEventType::Moved: {
FileInfo file( event.directory + event.filename, false ); FileInfo file =
preparedFile ? *preparedFile : FileInfo( event.directory + event.filename, false );
const std::string oldFilePath = FileSystem::isRelativePath( event.oldFilename ) const std::string oldFilePath = FileSystem::isRelativePath( event.oldFilename )
? event.directory + event.oldFilename ? event.directory + event.oldFilename
: event.oldFilename; : event.oldFilename;
if ( !file.exists() ) if ( !preparedFile && !file.exists() )
return false; return false;
auto* node = getNodeFromPath( oldFilePath, false, false ); auto* node = getNodeFromPath( oldFilePath, false, false );
if ( !node ) { if ( !node ) {
return handleFileEventLocked( return handleFileEventLocked(
{ FileSystemEventType::Add, event.directory, event.filename } ); { FileSystemEventType::Add, event.directory, event.filename }, preparedFile );
} }
ModelIndex index = node->index( *this, 0 ); ModelIndex index = node->index( *this, 0 );
@@ -1050,6 +1058,14 @@ void FileSystemModel::setupColumnNames( Translator* translator ) {
} }
bool FileSystemModel::handleFileEvent( const FileEvent& event ) { bool FileSystemModel::handleFileEvent( const FileEvent& event ) {
return handleFileEvent( event, nullptr );
}
bool FileSystemModel::handleFileEvent( const FileEvent& event, const FileInfo& file ) {
return handleFileEvent( event, &file );
}
bool FileSystemModel::handleFileEvent( const FileEvent& event, const FileInfo* preparedFile ) {
if ( !mInitOK ) if ( !mInitOK )
return false; return false;
@@ -1064,7 +1080,7 @@ bool FileSystemModel::handleFileEvent( const FileEvent& event ) {
{ {
Lock l( resourceMutex() ); Lock l( resourceMutex() );
ret = handleFileEventLocked( event ); ret = handleFileEventLocked( event, preparedFile );
} }
if ( ret ) if ( ret )
+31 -7
View File
@@ -9,6 +9,7 @@
#include <eepp/ui/doc/syntaxdefinitionmanager.hpp> #include <eepp/ui/doc/syntaxdefinitionmanager.hpp>
#include <eepp/ui/doc/textdocument.hpp> #include <eepp/ui/doc/textdocument.hpp>
#include <eepp/ui/tools/uidiffview.hpp> #include <eepp/ui/tools/uidiffview.hpp>
#include <eepp/ui/tools/uidocfindreplace.hpp>
#include <eepp/ui/tools/uiimageviewer.hpp> #include <eepp/ui/tools/uiimageviewer.hpp>
#include <eepp/ui/uiimage.hpp> #include <eepp/ui/uiimage.hpp>
#include <eepp/ui/uiscenenode.hpp> #include <eepp/ui/uiscenenode.hpp>
@@ -378,6 +379,14 @@ UIDiffView::UIDiffView() :
mLeftPlugin->registerUpdate( mLeftEditor ); mLeftPlugin->registerUpdate( mLeftEditor );
} ); } );
for ( auto* editor : { mEditor, mLeftEditor, mRightEditor } ) {
editor->on( Event::OnSizeChange, [this]( auto ) { onAutoSize(); } );
editor->on( Event::OnShowFindReplace, [this]( auto ) { updateButtonsVisibility(); } );
editor->on( Event::OnHideFindReplace, [this]( auto ) { updateButtonsVisibility(); } );
}
mLeftEditor->setVisible( false ); mLeftEditor->setVisible( false );
mRightEditor->setVisible( false ); mRightEditor->setVisible( false );
mLeftEditor->setVerticalScrollBarEnabled( false ); mLeftEditor->setVerticalScrollBarEnabled( false );
@@ -519,7 +528,7 @@ void UIDiffView::setViewMode( ViewMode mode ) {
void UIDiffView::setViewModeToggleVisible( bool visible ) { void UIDiffView::setViewModeToggleVisible( bool visible ) {
mViewModeToggleVisible = visible; mViewModeToggleVisible = visible;
mModeToggle->setVisible( visible ); updateButtonsVisibility();
updateModeButton(); updateModeButton();
} }
@@ -537,7 +546,7 @@ void UIDiffView::setCompleteView( bool complete ) {
void UIDiffView::setCompleteViewToggleVisible( bool visible ) { void UIDiffView::setCompleteViewToggleVisible( bool visible ) {
mCompleteViewToggleVisible = visible; mCompleteViewToggleVisible = visible;
mCompleteViewToggle->setVisible( visible ); updateButtonsVisibility();
updateModeButton(); updateModeButton();
} }
@@ -590,9 +599,10 @@ void UIDiffView::onAutoSize() {
return; return;
if ( mEditor && mLeftEditor && !mIsImageDiff ) { if ( mEditor && mLeftEditor && !mIsImageDiff ) {
setPixelsSize( getPixelsSize().getWidth(), mViewMode == ViewMode::Unified setPixelsSize( getPixelsSize().getWidth(),
? mEditor->getPixelsSize().getHeight() std::ceil( mViewMode == ViewMode::Unified
: mLeftEditor->getPixelsSize().getHeight() ); ? mEditor->getPixelsSize().getHeight()
: mLeftEditor->getPixelsSize().getHeight() ) );
} }
if ( mIsImageDiff && mLeftImageViewer && mRightImageViewer && mDiffImageViewer ) { if ( mIsImageDiff && mLeftImageViewer && mRightImageViewer && mDiffImageViewer ) {
@@ -610,7 +620,7 @@ void UIDiffView::onAutoSize() {
height = std::max( height, viewImageHeight( mRightImageViewer ) ); height = std::max( height, viewImageHeight( mRightImageViewer ) );
if ( displayDiffImage ) if ( displayDiffImage )
height = std::max( height, viewImageHeight( mDiffImageViewer ) ); height = std::ceil( std::max( height, viewImageHeight( mDiffImageViewer ) ) );
setPixelsSize( getPixelsSize().getWidth(), height ); setPixelsSize( getPixelsSize().getWidth(), height );
} }
@@ -1281,12 +1291,26 @@ void UIDiffView::loadFromFile( const std::string& oldFilePath, const std::string
loadFromStrings( oldText, newText ); loadFromStrings( oldText, newText );
} }
void UIDiffView::updateButtonsVisibility() {
bool findReplaceVisible{ false };
for ( const auto* editor : { mEditor, mLeftEditor, mRightEditor } ) {
const auto* findReplace = editor->getFindReplace();
if ( editor->isVisible() && findReplace && findReplace->isVisible() ) {
findReplaceVisible = true;
break;
}
}
mModeToggle->setVisible( mViewModeToggleVisible && !findReplaceVisible );
mCompleteViewToggle->setVisible( mCompleteViewToggleVisible && !findReplaceVisible &&
!mIsImageDiff );
}
void UIDiffView::updateButtonsText() { void UIDiffView::updateButtonsText() {
mModeToggle->setText( i18n( "diffview_side_by_side", "Side by Side" ) ); mModeToggle->setText( i18n( "diffview_side_by_side", "Side by Side" ) );
mModeToggle->setSelected( mViewMode != ViewMode::Unified ); mModeToggle->setSelected( mViewMode != ViewMode::Unified );
mCompleteViewToggle->setText( i18n( "diffview_compact", "Compact" ) ); mCompleteViewToggle->setText( i18n( "diffview_compact", "Compact" ) );
mCompleteViewToggle->setVisible( !mIsImageDiff );
mCompleteViewToggle->setSelected( !mShowCompleteView ); mCompleteViewToggle->setSelected( !mShowCompleteView );
updateButtonsVisibility();
} }
void UIDiffView::setSyntaxColorScheme( const SyntaxColorScheme& colorScheme ) { void UIDiffView::setSyntaxColorScheme( const SyntaxColorScheme& colorScheme ) {
+1 -1
View File
@@ -107,7 +107,7 @@ const char DOC_FIND_REPLACE_CSS[] = R"css(
)css"; )css";
const char DOC_FIND_REPLACE_XML[] = R"xml( const char DOC_FIND_REPLACE_XML[] = R"xml(
<hbox class="ce_find_replace_box" layout_width="wrap_content" layout_height="wrap_content" layout_gravity="right|top" layout-margin-right="32dp"> <hbox class="ce_find_replace_box" layout_width="wrap_content" layout_height="wrap_content" layout_gravity="right|top" margin_right="32dp">
<Widget class="expander" layout_width="2dp" layout_height="match_parent" /> <Widget class="expander" layout_width="2dp" layout_height="match_parent" />
<PushButton class="find_replace_toggle" layout_width="16dp" layout_height="wrap_content" padding="2dp" layout_gravity="center" /> <PushButton class="find_replace_toggle" layout_width="16dp" layout_height="wrap_content" padding="2dp" layout_gravity="center" />
<vbox layout_width="wrap_content" layout_height="wrap_content"> <vbox layout_width="wrap_content" layout_height="wrap_content">
+20 -4
View File
@@ -2170,8 +2170,14 @@ void UICodeEditor::updateScrollBar() {
bool showHScroll = bool showHScroll =
mLongestLineWidth > viewPortWidth && mLongestLineWidth > viewPortWidth &&
( !mDocView.isWrapEnabled() || mLineWrapType == LineWrapType::LineBreakingColumn ); ( !mDocView.isWrapEnabled() || mLineWrapType == LineWrapType::LineBreakingColumn );
bool wasVisible = mHScrollBar->isVisible();
mHScrollBar->setEnabled( showHScroll ); mHScrollBar->setEnabled( showHScroll );
mHScrollBar->setVisible( showHScroll ); mHScrollBar->setVisible( showHScroll );
if ( wasVisible != showHScroll ) {
// Must be updated because the horizontal scrollbar affects the visible lines count
notVisibleLineCount = (Int64)getTotalVisibleLines() - (Int64)getViewPortLineCount().y;
}
} }
mVScrollBar->setPixelsPosition( mSize.getWidth() - mVScrollBar->getPixelsSize().getWidth(), mVScrollBar->setPixelsPosition( mSize.getWidth() - mVScrollBar->getPixelsSize().getWidth(),
@@ -2190,6 +2196,8 @@ void UICodeEditor::updateScrollBar() {
setScrollY( mScroll.y ); setScrollY( mScroll.y );
mUpdatingScrollBar = false; mUpdatingScrollBar = false;
onAutoSize();
} }
void UICodeEditor::goToLine( const TextPosition& position, bool centered, bool forceExactPosition, void UICodeEditor::goToLine( const TextPosition& position, bool centered, bool forceExactPosition,
@@ -4902,8 +4910,13 @@ void UICodeEditor::showFindReplace() {
if ( !mFindReplaceEnabled ) if ( !mFindReplaceEnabled )
return; return;
if ( nullptr == mFindReplace ) if ( nullptr == mFindReplace ) {
mFindReplace = UIDocFindReplace::New( this, mDoc ); mFindReplace = UIDocFindReplace::New( this, mDoc );
mFindReplace->on( Event::OnVisibleChange, [this]( auto ) {
sendCommonEvent( mFindReplace->isVisible() ? Event::OnShowFindReplace
: Event::OnHideFindReplace );
} );
}
mFindReplace->setReplaceDisabled( mLocked ); mFindReplace->setReplaceDisabled( mLocked );
mFindReplace->show(); mFindReplace->show();
@@ -5890,10 +5903,13 @@ void UICodeEditor::loadFromXmlNode( const pugi::xml_node& node ) {
void UICodeEditor::onAutoSize() { void UICodeEditor::onAutoSize() {
if ( mHeightPolicy == SizePolicy::WrapContent ) { if ( mHeightPolicy == SizePolicy::WrapContent ) {
auto visibleLineCount = getDocumentView().getVisibleLinesCount(); auto visibleLineCount = getTotalVisibleLines();
Float lineHeight = getLineHeight(); Float lineHeight = getLineHeight();
Float height = lineHeight * visibleLineCount + getPixelsPadding().Top + Float height = std::ceil( lineHeight * visibleLineCount + mPaddingPx.Top +
getPixelsPadding().Bottom + getTotalTopSpace(); mPaddingPx.Bottom + getTotalTopSpace() );
if ( mHorizontalScrollBarEnabled && mHScrollBar->isVisible() ) {
height += std::ceil( mHScrollBar->getPixelsSize().getHeight() );
}
setPixelsSize( getPixelsSize().getWidth(), height ); setPixelsSize( getPixelsSize().getWidth(), height );
} }
} }
@@ -0,0 +1,108 @@
#include "../../tools/ecode/boundedeventqueue.hpp"
#include "utest.h"
#include <algorithm>
#include <atomic>
#include <thread>
#include <vector>
using namespace ecode;
UTEST( BoundedEventQueue, schedulesOnceAndDrainsInFifoOrder ) {
BoundedEventQueue<int> queue;
EXPECT_TRUE( queue.push( 1 ) );
EXPECT_FALSE( queue.push( 2 ) );
EXPECT_FALSE( queue.push( 3 ) );
std::vector<int> firstDrain;
queue.popUpTo( firstDrain, 2 );
EXPECT_EQ( firstDrain.size(), 2u );
EXPECT_EQ( firstDrain[0], 1 );
EXPECT_EQ( firstDrain[1], 2 );
EXPECT_TRUE( queue.finishDrain() );
std::vector<int> secondDrain;
queue.popUpTo( secondDrain, 2 );
EXPECT_EQ( secondDrain.size(), 1u );
EXPECT_EQ( secondDrain[0], 3 );
EXPECT_FALSE( queue.finishDrain() );
EXPECT_TRUE( queue.push( 4 ) );
}
UTEST( BoundedEventQueue, producerDuringDrainCannotLoseWakeup ) {
BoundedEventQueue<int> queue;
EXPECT_TRUE( queue.push( 1 ) );
std::vector<int> drain;
queue.popUpTo( drain, 1 );
EXPECT_FALSE( queue.push( 2 ) );
EXPECT_TRUE( queue.finishDrain() );
drain.clear();
queue.popUpTo( drain, 1 );
EXPECT_EQ( drain.size(), 1u );
EXPECT_EQ( drain[0], 2 );
EXPECT_FALSE( queue.finishDrain() );
}
UTEST( BoundedEventQueue, replacesOnlyMatchingLastPendingEvent ) {
BoundedEventQueue<int> queue;
EXPECT_TRUE( queue.pushOrReplaceLast( 1, []( int, int ) { return false; } ) );
EXPECT_FALSE( queue.pushOrReplaceLast( 2, []( int previous, int ) { return previous == 1; } ) );
EXPECT_FALSE( queue.pushOrReplaceLast( 3, []( int, int ) { return false; } ) );
std::vector<int> drain;
queue.popUpTo( drain, 4 );
EXPECT_EQ( drain.size(), 2u );
EXPECT_EQ( drain[0], 2 );
EXPECT_EQ( drain[1], 3 );
EXPECT_FALSE( queue.finishDrain() );
}
UTEST( BoundedEventQueue, acceptsConcurrentProducersWithoutLoss ) {
static constexpr int ProducerCount = 4;
static constexpr int EventsPerProducer = 1000;
BoundedEventQueue<int> queue;
std::atomic<int> scheduleCount{ 0 };
std::vector<std::thread> producers;
for ( int producer = 0; producer < ProducerCount; ++producer ) {
producers.emplace_back( [producer, &queue, &scheduleCount] {
for ( int event = 0; event < EventsPerProducer; ++event ) {
if ( queue.push( producer * EventsPerProducer + event ) )
++scheduleCount;
}
} );
}
for ( auto& producer : producers )
producer.join();
EXPECT_EQ( scheduleCount.load(), 1 );
std::vector<int> received;
do {
queue.popUpTo( received, received.size() + 37 );
} while ( queue.finishDrain() );
EXPECT_EQ( received.size(), static_cast<std::size_t>( ProducerCount * EventsPerProducer ) );
int lastEvent[ProducerCount] = { -1, -1, -1, -1 };
for ( int event : received ) {
const int producer = event / EventsPerProducer;
EXPECT_EQ( event, producer * EventsPerProducer + ++lastEvent[producer] );
}
std::sort( received.begin(), received.end() );
for ( int event = 0; event < ProducerCount * EventsPerProducer; ++event )
EXPECT_EQ( received[event], event );
}
UTEST( BoundedEventQueue, clearCancelsPendingDrainState ) {
BoundedEventQueue<int> queue;
EXPECT_TRUE( queue.push( 1 ) );
EXPECT_FALSE( queue.push( 2 ) );
queue.clear();
EXPECT_TRUE( queue.push( 3 ) );
std::vector<int> drain;
queue.popUpTo( drain, 4 );
EXPECT_EQ( drain.size(), 1u );
EXPECT_EQ( drain[0], 3 );
EXPECT_FALSE( queue.finishDrain() );
}
@@ -0,0 +1,94 @@
#include "../../tools/ecode/filesystemlistener.hpp"
#include "utest.h"
#include <condition_variable>
#include <future>
#include <mutex>
#include <thread>
using namespace ecode;
UTEST( FileSystemListenerOptions, filtersByEventTypeAndPathPrefix ) {
FileSystemListener::ListenerOptions options;
FileSystemListenerFilter filter;
filter.eventTypes = FileSystemListener::eventTypeMask( FileSystemEventType::Add ) |
FileSystemListener::eventTypeMask( FileSystemEventType::Modified );
filter.path = "/tmp/ecode-ipc/";
options.filters.emplace_back( std::move( filter ) );
EXPECT_TRUE( options.matches( FileSystemEventType::Add, "/tmp/ecode-ipc/request" ) );
EXPECT_TRUE( options.matches( FileSystemEventType::Modified, "/tmp/ecode-ipc/request" ) );
EXPECT_FALSE( options.matches( FileSystemEventType::Delete, "/tmp/ecode-ipc/request" ) );
EXPECT_FALSE( options.matches( FileSystemEventType::Add, "/tmp/other/request" ) );
}
UTEST( FileSystemListenerOptions, matchesAnyFilterWithoutDuplicateSemantics ) {
FileSystemListener::ListenerOptions options;
FileSystemListenerFilter config;
config.eventTypes = FileSystemListener::eventTypeMask( FileSystemEventType::Modified );
config.path = "/tmp/plugin.json";
config.pathMatch = FileEventPathMatch::Exact;
options.filters.emplace_back( std::move( config ) );
FileSystemListenerFilter workspace;
workspace.path = "/tmp/workspace/";
options.filters.emplace_back( std::move( workspace ) );
EXPECT_TRUE( options.matches( FileSystemEventType::Modified, "/tmp/plugin.json" ) );
EXPECT_FALSE( options.matches( FileSystemEventType::Add, "/tmp/plugin.json" ) );
EXPECT_TRUE( options.matches( FileSystemEventType::Add, "/tmp/workspace/file.cpp" ) );
EXPECT_FALSE( options.matches( FileSystemEventType::Modified, "/tmp/plugin.json.backup" ) );
EXPECT_TRUE(
options.matchesJoinedPath( FileSystemEventType::Moved, "/tmp/workspace/", 0, "file.cpp" ) );
EXPECT_TRUE( options.matchesJoinedPath( FileSystemEventType::Moved, "/tmp/workspace", '/',
"file.cpp" ) );
}
UTEST( FileSystemListenerOptions, defaultsToAllEventsAndPathsOnMainThread ) {
FileSystemListener::ListenerOptions options;
EXPECT_EQ( options.affinity, FileSystemListener::ThreadAffinity::Main );
EXPECT_TRUE( options.matches( FileSystemEventType::Add, "/any/path" ) );
EXPECT_TRUE( options.matches( FileSystemEventType::Delete, "/another/path" ) );
EXPECT_TRUE( options.matches( FileSystemEventType::Modified, "relative/path" ) );
EXPECT_TRUE( options.matches( FileSystemEventType::Moved, "" ) );
}
UTEST( FileSystemListener, removalWaitsForActiveCallback ) {
FileSystemListenerCallbackState callbackState;
std::mutex mutex;
std::condition_variable condition;
bool callbackBegan{ false };
bool callbackStarted{ false };
bool releaseCallback{ false };
std::thread callbackThread( [&] {
callbackBegan = callbackState.beginCallback();
std::unique_lock<std::mutex> lock( mutex );
callbackStarted = true;
condition.notify_all();
condition.wait( lock, [&] { return releaseCallback; } );
lock.unlock();
callbackState.endCallback();
} );
{
std::unique_lock<std::mutex> lock( mutex );
condition.wait( lock, [&] { return callbackStarted; } );
}
auto removal = std::async( std::launch::async, [&] { callbackState.removeAndWait( false ); } );
EXPECT_EQ( removal.wait_for( std::chrono::milliseconds( 20 ) ), std::future_status::timeout );
{
std::lock_guard<std::mutex> lock( mutex );
releaseCallback = true;
}
condition.notify_all();
callbackThread.join();
removal.get();
EXPECT_TRUE( callbackBegan );
EXPECT_FALSE( callbackState.beginCallback() );
}
UTEST( FileSystemListener, callbackCanRemoveItself ) {
FileSystemListenerCallbackState callbackState;
ASSERT_TRUE( callbackState.beginCallback() );
callbackState.removeAndWait( true );
callbackState.endCallback();
EXPECT_FALSE( callbackState.beginCallback() );
}
@@ -298,6 +298,29 @@ UTEST( FileSystemModelMove, keepsUnopenedDestinationBranchLazy ) {
nullptr ); nullptr );
} }
UTEST( FileSystemModelEvents, preparedMetadataSurvivesDelayedAddDelivery ) {
TempTree tree;
auto directory = tree.path / "directory";
auto path = directory / "transient.txt";
std::filesystem::create_directories( directory );
auto model = FileSystemModel::New( tree.path.string() );
ASSERT_TRUE( model->getNodeFromPath( directory.string(), true ) != nullptr );
std::FILE* file = std::fopen( path.string().c_str(), "wb" );
ASSERT_TRUE( file != nullptr );
std::fclose( file );
FileInfo preparedFile( path.string(), false );
ASSERT_TRUE( preparedFile.exists() );
std::filesystem::remove( path );
ASSERT_TRUE(
model->handleFileEvent( { FileSystemEventType::Add,
directory.string() + FileSystem::getOSSlash(), "transient.txt" },
preparedFile ) );
ASSERT_TRUE( model->getNodeFromPath( path.string(), false, false ) != nullptr );
}
static ModelIndex indexOfNode( const FileSystemModel& model, const void* node, static ModelIndex indexOfNode( const FileSystemModel& model, const void* node,
const ModelIndex& parent = {} ) { const ModelIndex& parent = {} ) {
for ( Int64 row = 0; row < (Int64)model.rowCount( parent ); ++row ) { for ( Int64 row = 0; row < (Int64)model.rowCount( parent ); ++row ) {
+69
View File
@@ -0,0 +1,69 @@
#ifndef ECODE_BOUNDEDEVENTQUEUE_HPP
#define ECODE_BOUNDEDEVENTQUEUE_HPP
#include <cstddef>
#include <deque>
#include <mutex>
#include <utility>
namespace ecode {
// Thread-safe producer queue with a single-consumer scheduling handshake.
// push() returns true exactly when the caller must schedule the consumer.
// Once a drain completes, finishDrain() either transfers responsibility to
// the next drain or atomically makes the next producer responsible for it.
template <typename T> class BoundedEventQueue {
public:
bool push( T&& event ) {
std::lock_guard<std::mutex> lock( mMutex );
mEvents.emplace_back( std::move( event ) );
if ( mDrainScheduled )
return false;
mDrainScheduled = true;
return true;
}
template <typename Predicate> bool pushOrReplaceLast( T&& event, Predicate&& shouldReplace ) {
std::lock_guard<std::mutex> lock( mMutex );
if ( !mEvents.empty() && shouldReplace( mEvents.back(), event ) ) {
mEvents.back() = std::move( event );
return false;
}
mEvents.emplace_back( std::move( event ) );
if ( mDrainScheduled )
return false;
mDrainScheduled = true;
return true;
}
template <typename Container> void popUpTo( Container& events, std::size_t maxEvents ) {
std::lock_guard<std::mutex> lock( mMutex );
while ( !mEvents.empty() && events.size() < maxEvents ) {
events.emplace_back( std::move( mEvents.front() ) );
mEvents.pop_front();
}
}
bool finishDrain() {
std::lock_guard<std::mutex> lock( mMutex );
if ( !mEvents.empty() )
return true;
mDrainScheduled = false;
return false;
}
void clear() {
std::lock_guard<std::mutex> lock( mMutex );
mEvents.clear();
mDrainScheduled = false;
}
private:
std::mutex mMutex;
std::deque<T> mEvents;
bool mDrainScheduled{ false };
};
} // namespace ecode
#endif // ECODE_BOUNDEDEVENTQUEUE_HPP
+12 -7
View File
@@ -5059,12 +5059,16 @@ void App::init( InitParameters& params ) {
mFileWatcher->addWatch( mPidPath, mFileSystemListener ); mFileWatcher->addWatch( mPidPath, mFileSystemListener );
mFileWatcher->watch(); mFileWatcher->watch();
mPluginManager->setFileSystemListener( mFileSystemListener ); mPluginManager->setFileSystemListener( mFileSystemListener );
mIpcListenerId = FileSystemListener::ListenerOptions ipcListenerOptions;
mFileSystemListener->addListener( [this]( const FileEvent& fe, const FileInfo& fi ) { FileSystemListenerFilter ipcListenerFilter;
if ( !( ( fe.type == FileSystemEventType::Add || ipcListenerFilter.eventTypes =
fe.type == FileSystemEventType::Modified ) && FileSystemListener::eventTypeMask( FileSystemEventType::Add ) |
fe.directory == mPidPath ) ) FileSystemListener::eventTypeMask( FileSystemEventType::Modified );
return; ipcListenerFilter.path = mPidPath;
ipcListenerOptions.filters.emplace_back( std::move( ipcListenerFilter ) );
ipcListenerOptions.affinity = FileSystemListener::ThreadAffinity::Worker;
mIpcListenerId = mFileSystemListener->addListener(
[this]( const FileEvent&, const FileInfo& fi ) {
std::string path; std::string path;
FileSystem::fileGet( fi.getFilepath(), path ); FileSystem::fileGet( fi.getFilepath(), path );
String::trimInPlace( path, ' ' ); String::trimInPlace( path, ' ' );
@@ -5091,7 +5095,8 @@ void App::init( InitParameters& params ) {
} ); } );
} }
FileSystem::fileRemove( fi.getFilepath() ); FileSystem::fileRemove( fi.getFilepath() );
} ); },
std::move( ipcListenerOptions ) );
#endif #endif
mNotificationCenter = std::make_unique<NotificationCenter>( mNotificationCenter = std::make_unique<NotificationCenter>(
+160 -42
View File
@@ -1,4 +1,5 @@
#include "filesystemlistener.hpp" #include "filesystemlistener.hpp"
#include <eepp/core/small_vector.hpp>
#include <eepp/scene/actionmanager.hpp> #include <eepp/scene/actionmanager.hpp>
#include <eepp/scene/scenemanager.hpp> #include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp> #include <eepp/system/filesystem.hpp>
@@ -8,6 +9,26 @@
namespace ecode { namespace ecode {
static thread_local const void* currentFileSystemListenerCallback{ nullptr };
class FileSystemListenerCallbackGuard {
public:
FileSystemListenerCallbackGuard( FileSystemListenerCallbackState& state,
const void* listener ) :
mState( state ), mPreviousCallback( currentFileSystemListenerCallback ) {
currentFileSystemListenerCallback = listener;
}
~FileSystemListenerCallbackGuard() {
currentFileSystemListenerCallback = mPreviousCallback;
mState.endCallback();
}
private:
FileSystemListenerCallbackState& mState;
const void* mPreviousCallback;
};
std::string getFileSystemEventTypeName( FileSystemEventType action ) { std::string getFileSystemEventTypeName( FileSystemEventType action ) {
switch ( action ) { switch ( action ) {
case FileSystemEventType::Add: case FileSystemEventType::Add:
@@ -45,6 +66,17 @@ FileSystemListener::~FileSystemListener() {
// block instead of an atomic flag. // block instead of an atomic flag.
eeASSERT( !Engine::existsSingleton() || Engine::isMainThread() ); eeASSERT( !Engine::existsSingleton() || Engine::isMainThread() );
*mLifetime = false; *mLifetime = false;
SmallVector<std::shared_ptr<Listener>, 8> listeners;
{
Lock l( mCbsMutex );
for ( auto& listener : mCbs )
listeners.emplace_back( std::move( listener.second ) );
mCbs.clear();
}
for ( const auto& listener : listeners )
listener->callbackState.removeAndWait( currentFileSystemListenerCallback ==
listener.get() );
mPendingEvents.clear();
if ( auto* scene = SceneManager::instance()->getUISceneNode() ) if ( auto* scene = SceneManager::instance()->getUISceneNode() )
scene->getActionManager()->removeActionsByTagFromTarget( scene, mEventActionTag ); scene->getActionManager()->removeActionsByTagFromTarget( scene, mEventActionTag );
} }
@@ -56,35 +88,78 @@ static inline bool endsWithSlash( const std::string& dir ) {
void FileSystemListener::handleFileAction( efsw::WatchID, const std::string& dir, void FileSystemListener::handleFileAction( efsw::WatchID, const std::string& dir,
const std::string& filename, efsw::Action action, const std::string& filename, efsw::Action action,
const std::string& oldFilename ) { const std::string& oldFilename ) {
// The whole logical event (model, directory tree, open documents, eeASSERT( !Engine::isMainThread() );
// callbacks) must run on the main thread in its original order: the model FileEvent event( (FileSystemEventType)action, dir, filename, oldFilename );
// update used to complete synchronously before the other consequences, and FileInfo file( ( endsWithSlash( dir ) ? dir : ( dir + FileSystem::getOSSlash() ) ) + filename,
// all of them touch UI-owned state. The destructor cancels queued actions false );
// (tagged with mEventActionTag), so a runnable can never outlive the std::shared_ptr<ProjectDirectoryTree> dirTree;
// listener. {
if ( !Engine::isMainThread() ) { Lock l( mDirTreeMutex );
dirTree = mDirTree;
}
if ( dirTree && action != efsw::Actions::Modified )
dirTree->onChange( (ProjectDirectoryTree::Action)action, file, oldFilename );
// Worker listeners receive the event early. The queued phase is independent:
// it updates the model and documents and then dispatches main-thread listeners.
dispatchCallbacks( ThreadAffinity::Worker, event, file, file.getFilepath(), true );
enqueueFileAction( { std::move( event ), std::move( file ), action } );
}
void FileSystemListener::enqueueFileAction( PendingFileAction&& event ) {
auto* scene = SceneManager::instance()->getUISceneNode();
if ( !scene )
return;
const bool schedule = mPendingEvents.pushOrReplaceLast(
std::move( event ), []( const PendingFileAction& previous, const PendingFileAction& next ) {
return previous.action == efsw::Actions::Modified &&
next.action == efsw::Actions::Modified &&
previous.file.getFilepath() == next.file.getFilepath();
} );
if ( schedule ) {
scene->runOnMainThread(
[lifetime = mLifetime, this]() {
if ( lifetime->load( std::memory_order_acquire ) )
drainFileActions();
},
Time::Zero, mEventActionTag );
}
}
void FileSystemListener::drainFileActions() {
eeASSERT( Engine::isMainThread() );
static constexpr std::size_t MaxEventsPerDrain = 64;
SmallVector<PendingFileAction, MaxEventsPerDrain> events;
mPendingEvents.popUpTo( events, MaxEventsPerDrain );
for ( auto& event : events )
processFileAction( std::move( event ) );
if ( mPendingEvents.finishDrain() ) {
if ( auto* scene = SceneManager::instance()->getUISceneNode() ) { if ( auto* scene = SceneManager::instance()->getUISceneNode() ) {
scene->runOnMainThread( scene->runOnMainThread(
[lifetime = mLifetime, this, dir, filename, action, oldFilename]() { [lifetime = mLifetime, this]() {
if ( !lifetime->load( std::memory_order_acquire ) ) if ( lifetime->load( std::memory_order_acquire ) )
return; drainFileActions();
handleFileAction( 0, dir, filename, action, oldFilename );
}, },
Time::Zero, mEventActionTag ); Time::Zero, mEventActionTag );
return; } else {
mPendingEvents.clear();
} }
// No scene node: cannot safely touch UI state from this thread.
return;
} }
}
FileInfo file( ( endsWithSlash( dir ) ? dir : ( dir + FileSystem::getOSSlash() ) ) + filename ); void FileSystemListener::processFileAction( PendingFileAction&& pending ) {
eeASSERT( Engine::isMainThread() );
FileEvent& event = pending.event;
const std::string& eventPath = pending.file.getFilepath();
FileInfo file( pending.file );
const efsw::Action action = pending.action;
switch ( action ) { switch ( action ) {
case efsw::Actions::Add: case efsw::Actions::Add:
case efsw::Actions::Delete: case efsw::Actions::Delete:
case efsw::Actions::Moved: { case efsw::Actions::Moved: {
FileEvent event( (FileSystemEventType)action, dir, filename, oldFilename );
if ( Log::instance() && Log::instance()->getLogLevelThreshold() == LogLevel::Debug ) { if ( Log::instance() && Log::instance()->getLogLevelThreshold() == LogLevel::Debug ) {
std::string txt = std::string txt =
"DIR ( " + event.directory + " ) FILE ( " + "DIR ( " + event.directory + " ) FILE ( " +
@@ -97,14 +172,12 @@ void FileSystemListener::handleFileAction( efsw::WatchID, const std::string& dir
} }
if ( mFileSystemModel ) if ( mFileSystemModel )
mFileSystemModel->handleFileEvent( event ); mFileSystemModel->handleFileEvent( event, file );
if ( mDirTree )
mDirTree->onChange( (ProjectDirectoryTree::Action)action, file, oldFilename );
if ( action == efsw::Actions::Moved ) { if ( action == efsw::Actions::Moved ) {
FileInfo oldFile( FileSystem::isRelativePath( oldFilename ) ? dir + oldFilename FileInfo oldFile( FileSystem::isRelativePath( event.oldFilename )
: oldFilename ); ? event.directory + event.oldFilename
: event.oldFilename );
if ( file.isLink() ) if ( file.isLink() )
file = FileInfo( file.linksTo() ); file = FileInfo( file.linksTo() );
@@ -125,11 +198,7 @@ void FileSystemListener::handleFileAction( efsw::WatchID, const std::string& dir
if ( isFileOpen( file ) ) if ( isFileOpen( file ) )
notifyChange( file ); notifyChange( file );
Lock l( mCbsMutex ); dispatchCallbacks( ThreadAffinity::Main, event, file, eventPath );
if ( !mCbs.empty() ) {
for ( const auto& cb : mCbs )
cb.second( event, file );
}
break; break;
} }
@@ -139,35 +208,84 @@ void FileSystemListener::handleFileAction( efsw::WatchID, const std::string& dir
if ( isFileOpen( file ) ) if ( isFileOpen( file ) )
notifyChange( file ); notifyChange( file );
Lock l( mCbsMutex ); dispatchCallbacks( ThreadAffinity::Main, event, file, eventPath );
if ( !mCbs.empty() ) {
FileEvent event( (FileSystemEventType)action, dir, filename, oldFilename );
for ( const auto& cb : mCbs )
cb.second( event, file );
}
} }
} }
} }
void FileSystemListener::setDirTree( const std::shared_ptr<ProjectDirectoryTree>& dirTree ) { void FileSystemListener::setDirTree( const std::shared_ptr<ProjectDirectoryTree>& dirTree ) {
Lock l( mDirTreeMutex );
mDirTree = dirTree; mDirTree = dirTree;
} }
Uint64 FileSystemListener::addListener( const FileEventFn& fn ) { Uint64 FileSystemListener::addListener( const FileEventFn& fn ) {
return addListener( fn, {} );
}
Uint64 FileSystemListener::addListener( const FileEventFn& fn, ListenerOptions options ) {
Lock l( mCbsMutex ); Lock l( mCbsMutex );
Uint64 id = ++mLastId; Uint64 id = ++mLastId;
mCbs[id] = fn; mCbs[id] = std::make_shared<Listener>( fn, std::move( options ) );
return id; return id;
} }
bool FileSystemListener::removeListener( const Uint64& id ) { void FileSystemListener::dispatchCallbacks( ThreadAffinity affinity, const FileEvent& event,
Lock l( mCbsMutex ); const FileInfo& file, const std::string& eventPath,
auto it = mCbs.find( id ); bool resolveLinks ) {
if ( it != mCbs.end() ) { SmallVector<std::shared_ptr<Listener>, 8> callbacks;
mCbs.erase( it ); {
return true; Lock l( mCbsMutex );
for ( const auto& entry : mCbs ) {
const Listener& listener = *entry.second;
if ( listener.options.affinity != affinity )
continue;
bool matches = listener.options.matches( event.type, eventPath );
if ( !matches && event.type == FileSystemEventType::Moved &&
!event.oldFilename.empty() ) {
if ( FileSystem::isRelativePath( event.oldFilename ) ) {
matches = listener.options.matchesJoinedPath(
event.type, event.directory,
endsWithSlash( event.directory ) ? 0 : FileSystem::getOSSlash()[0],
event.oldFilename );
} else {
matches = listener.options.matches( event.type, event.oldFilename );
}
}
if ( matches )
callbacks.emplace_back( entry.second );
}
} }
return false; if ( callbacks.empty() )
return;
auto invokeCallback = [&]( const std::shared_ptr<Listener>& listener,
const FileInfo& callbackFile ) {
if ( !listener->callbackState.beginCallback() )
return;
FileSystemListenerCallbackGuard guard( listener->callbackState, listener.get() );
listener->callback( event, callbackFile );
};
if ( resolveLinks && file.isLink() ) {
FileInfo resolvedFile( file.linksTo() );
for ( const auto& listener : callbacks )
invokeCallback( listener, resolvedFile );
return;
}
for ( const auto& listener : callbacks )
invokeCallback( listener, file );
}
bool FileSystemListener::removeListener( const Uint64& id ) {
std::shared_ptr<Listener> listener;
{
Lock l( mCbsMutex );
auto it = mCbs.find( id );
if ( it == mCbs.end() )
return false;
listener = std::move( it->second );
mCbs.erase( it );
}
listener->callbackState.removeAndWait( currentFileSystemListenerCallback == listener.get() );
return true;
} }
bool FileSystemListener::isFileOpen( const FileInfo& file ) { bool FileSystemListener::isFileOpen( const FileInfo& file ) {
+66 -1
View File
@@ -1,8 +1,11 @@
#ifndef ECODE_FILESYSTEMLISTENER_HPP #ifndef ECODE_FILESYSTEMLISTENER_HPP
#define ECODE_FILESYSTEMLISTENER_HPP #define ECODE_FILESYSTEMLISTENER_HPP
#include "boundedeventqueue.hpp"
#include "filesystemlisteneroptions.hpp"
#include "projectdirectorytree.hpp" #include "projectdirectorytree.hpp"
#include <atomic> #include <atomic>
#include <condition_variable>
#include <eepp/system/fileinfo.hpp> #include <eepp/system/fileinfo.hpp>
#include <eepp/ui/models/filesystemmodel.hpp> #include <eepp/ui/models/filesystemmodel.hpp>
#include <eepp/ui/tools/uicodeeditorsplitter.hpp> #include <eepp/ui/tools/uicodeeditorsplitter.hpp>
@@ -17,9 +20,44 @@ using namespace EE::UI::Tools;
namespace ecode { namespace ecode {
class FileSystemListenerCallbackState {
public:
bool beginCallback() {
std::lock_guard<std::mutex> lock( mMutex );
if ( mRemoved )
return false;
++mActiveCallbacks;
return true;
}
void endCallback() {
std::lock_guard<std::mutex> lock( mMutex );
if ( --mActiveCallbacks == 0 )
mCondition.notify_all();
}
void removeAndWait( bool calledFromThisListener ) {
std::unique_lock<std::mutex> lock( mMutex );
mRemoved = true;
if ( !calledFromThisListener )
mCondition.wait( lock, [this] { return mActiveCallbacks == 0; } );
}
private:
std::mutex mMutex;
std::condition_variable mCondition;
std::size_t mActiveCallbacks{ 0 };
bool mRemoved{ false };
};
class FileSystemListener : public efsw::FileWatchListener { class FileSystemListener : public efsw::FileWatchListener {
public: public:
typedef std::function<void( const FileEvent&, const FileInfo& )> FileEventFn; typedef std::function<void( const FileEvent&, const FileInfo& )> FileEventFn;
using ThreadAffinity = FileEventThreadAffinity;
using ListenerOptions = FileSystemListenerOptions;
static constexpr FileEventTypeMask eventTypeMask( FileSystemEventType type ) {
return fileEventTypeMask( type );
}
FileSystemListener( UICodeEditorSplitter* codeSplitter, FileSystemListener( UICodeEditorSplitter* codeSplitter,
std::shared_ptr<FileSystemModel> fileSystemModel, std::shared_ptr<FileSystemModel> fileSystemModel,
@@ -36,22 +74,49 @@ class FileSystemListener : public efsw::FileWatchListener {
Uint64 addListener( const FileEventFn& fn ); Uint64 addListener( const FileEventFn& fn );
Uint64 addListener( const FileEventFn& fn, ListenerOptions options );
bool removeListener( const Uint64& id ); bool removeListener( const Uint64& id );
protected: protected:
struct PendingFileAction {
FileEvent event;
FileInfo file;
efsw::Action action;
};
struct Listener {
FileEventFn callback;
ListenerOptions options;
FileSystemListenerCallbackState callbackState;
Listener( FileEventFn callback, ListenerOptions options ) :
callback( std::move( callback ) ), options( std::move( options ) ) {}
};
UICodeEditorSplitter* mSplitter; UICodeEditorSplitter* mSplitter;
std::shared_ptr<FileSystemModel> mFileSystemModel; std::shared_ptr<FileSystemModel> mFileSystemModel;
std::shared_ptr<ProjectDirectoryTree> mDirTree; std::shared_ptr<ProjectDirectoryTree> mDirTree;
std::atomic<Uint64> mLastId{ 0 }; std::atomic<Uint64> mLastId{ 0 };
std::unordered_map<Uint64, FileEventFn> mCbs; std::unordered_map<Uint64, std::shared_ptr<Listener>> mCbs;
std::vector<std::string> mIgnoredFiles; std::vector<std::string> mIgnoredFiles;
Mutex mCbsMutex; Mutex mCbsMutex;
Mutex mDirTreeMutex;
BoundedEventQueue<PendingFileAction> mPendingEvents;
// Tag of the queued main-thread file-event actions; the destructor cancels // Tag of the queued main-thread file-event actions; the destructor cancels
// them and marks the lifetime token dead so a queued runnable can never // them and marks the lifetime token dead so a queued runnable can never
// dereference the destroyed listener. // dereference the destroyed listener.
Uint64 mEventActionTag{ 0 }; Uint64 mEventActionTag{ 0 };
std::shared_ptr<std::atomic<bool>> mLifetime; std::shared_ptr<std::atomic<bool>> mLifetime;
void enqueueFileAction( PendingFileAction&& event );
void drainFileActions();
void processFileAction( PendingFileAction&& event );
void dispatchCallbacks( ThreadAffinity affinity, const FileEvent& event, const FileInfo& file,
const std::string& eventPath, bool resolveLinks = false );
bool isFileOpen( const FileInfo& file ); bool isFileOpen( const FileInfo& file );
void notifyChange( const FileInfo& file ); void notifyChange( const FileInfo& file );
@@ -0,0 +1,92 @@
#ifndef ECODE_FILESYSTEMLISTENEROPTIONS_HPP
#define ECODE_FILESYSTEMLISTENEROPTIONS_HPP
#include <eepp/core.hpp>
#include <eepp/ui/models/filesystemmodel.hpp>
namespace ecode {
using namespace EE::UI::Models;
using FileEventTypeMask = EE::Uint32;
enum class FileEventThreadAffinity { Main, Worker };
enum class FileEventPathMatch { Prefix, Exact };
static constexpr FileEventTypeMask fileEventTypeMask( FileSystemEventType type ) {
return 1u << static_cast<EE::Uint32>( type );
}
struct FileSystemListenerFilter {
FileEventTypeMask eventTypes{ 0xFFFFFFFFu };
std::string path;
FileEventPathMatch pathMatch{ FileEventPathMatch::Prefix };
bool matches( FileSystemEventType type, const std::string& filePath ) const {
if ( ( eventTypes & fileEventTypeMask( type ) ) == 0 )
return false;
if ( path.empty() )
return true;
return pathMatch == FileEventPathMatch::Exact
? filePath == path
: filePath.compare( 0, path.size(), path ) == 0;
}
bool matchesJoinedPath( FileSystemEventType type, const std::string& directory, char separator,
const std::string& filename ) const {
if ( ( eventTypes & fileEventTypeMask( type ) ) == 0 )
return false;
if ( path.empty() )
return true;
const std::size_t joinedSize =
directory.size() + ( separator != 0 ? 1 : 0 ) + filename.size();
if ( path.size() > joinedSize ||
( pathMatch == FileEventPathMatch::Exact && path.size() != joinedSize ) )
return false;
std::size_t pathPos = 0;
auto matchesPart = [&]( const char* data, std::size_t size ) {
const std::size_t count = std::min( size, path.size() - pathPos );
if ( count != 0 && path.compare( pathPos, count, data, count ) != 0 )
return false;
pathPos += count;
return true;
};
if ( !matchesPart( directory.data(), directory.size() ) )
return false;
if ( separator != 0 && pathPos < path.size() ) {
if ( !matchesPart( &separator, 1 ) )
return false;
}
return pathPos == path.size() || matchesPart( filename.data(), filename.size() );
}
};
struct FileSystemListenerOptions {
std::vector<FileSystemListenerFilter> filters;
FileEventThreadAffinity affinity{ FileEventThreadAffinity::Main };
bool matches( FileSystemEventType type, const std::string& filePath ) const {
if ( filters.empty() )
return true;
for ( const auto& filter : filters ) {
if ( filter.matches( type, filePath ) )
return true;
}
return false;
}
bool matchesJoinedPath( FileSystemEventType type, const std::string& directory, char separator,
const std::string& filename ) const {
if ( filters.empty() )
return true;
for ( const auto& filter : filters ) {
if ( filter.matchesJoinedPath( type, directory, separator, filename ) )
return true;
}
return false;
}
};
} // namespace ecode
#endif // ECODE_FILESYSTEMLISTENEROPTIONS_HPP
@@ -674,6 +674,23 @@ void AutoCompletePlugin::onFileSystemEvent( const FileEvent& ev, const FileInfo&
} }
} }
FileSystemListenerOptions AutoCompletePlugin::getFileSystemListenerOptions() const {
auto options = Plugin::getFileSystemListenerOptions();
const auto eventTypes = fileEventTypeMask( FileSystemEventType::Add ) |
fileEventTypeMask( FileSystemEventType::Delete ) |
fileEventTypeMask( FileSystemEventType::Modified ) |
fileEventTypeMask( FileSystemEventType::Moved );
for ( const auto* path : { &mUserSnippetsPath, &mVSCodeSnippetsPath, &mEcodeSnippetsPath } ) {
if ( path->empty() )
continue;
FileSystemListenerFilter filter;
filter.eventTypes = eventTypes;
filter.path = *path;
options.filters.emplace_back( std::move( filter ) );
}
return options;
}
void AutoCompletePlugin::onRegister( UICodeEditor* editor ) { void AutoCompletePlugin::onRegister( UICodeEditor* editor ) {
registerSnippetLocatorProvider(); registerSnippetLocatorProvider();
Lock l( mDocMutex ); Lock l( mDocMutex );
@@ -109,6 +109,7 @@ class AutoCompletePlugin : public Plugin {
bool onMouseDoubleClick( UICodeEditor*, const Vector2i&, const Uint32& ) override; bool onMouseDoubleClick( UICodeEditor*, const Vector2i&, const Uint32& ) override;
bool onMouseMove( UICodeEditor*, const Vector2i&, const Uint32& ) override; bool onMouseMove( UICodeEditor*, const Vector2i&, const Uint32& ) override;
void onFileSystemEvent( const FileEvent&, const FileInfo& ) override; void onFileSystemEvent( const FileEvent&, const FileInfo& ) override;
FileSystemListenerOptions getFileSystemListenerOptions() const override;
void onLoadProject( const std::string& projectFolder, void onLoadProject( const std::string& projectFolder,
const std::string& projectStatePath ) override; const std::string& projectStatePath ) override;
+11
View File
@@ -562,6 +562,17 @@ void GitPlugin::onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) {
updateUI(); updateUI();
} }
FileSystemListenerOptions GitPlugin::getFileSystemListenerOptions() const {
auto options = Plugin::getFileSystemListenerOptions();
const auto& workspace = getManager()->getWorkspaceFolder();
if ( !workspace.empty() ) {
FileSystemListenerFilter filter;
filter.path = workspace;
options.filters.emplace_back( std::move( filter ) );
}
return options;
}
void GitPlugin::displayTooltip( UICodeEditor* editor, const Git::Blame& blame, void GitPlugin::displayTooltip( UICodeEditor* editor, const Git::Blame& blame,
const Vector2f& position ) { const Vector2f& position ) {
// HACK: Gets the old font style to restore it when the tooltip is hidden // HACK: Gets the old font style to restore it when the tooltip is hidden
@@ -54,6 +54,8 @@ class GitPlugin : public PluginBase {
void onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) override; void onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) override;
FileSystemListenerOptions getFileSystemListenerOptions() const override;
void onRegister( UICodeEditor* ) override; void onRegister( UICodeEditor* ) override;
void onUnregister( UICodeEditor* ) override; void onUnregister( UICodeEditor* ) override;
+10
View File
@@ -110,6 +110,16 @@ void Plugin::onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) {
} }
} }
FileSystemListenerOptions Plugin::getFileSystemListenerOptions() const {
FileSystemListenerOptions options;
FileSystemListenerFilter filter;
filter.eventTypes = fileEventTypeMask( FileSystemEventType::Modified );
filter.path = mConfigPath;
filter.pathMatch = FileEventPathMatch::Exact;
options.filters.emplace_back( std::move( filter ) );
return options;
}
void Plugin::setReady( Time loadTime ) { void Plugin::setReady( Time loadTime ) {
if ( mReady ) { if ( mReady ) {
if ( loadTime != Time::Zero ) { if ( loadTime != Time::Zero ) {
+3
View File
@@ -1,6 +1,7 @@
#ifndef ECODE_PLUGIN_HPP #ifndef ECODE_PLUGIN_HPP
#define ECODE_PLUGIN_HPP #define ECODE_PLUGIN_HPP
#include "../filesystemlisteneroptions.hpp"
#include "lsp/lspprotocol.hpp" #include "lsp/lspprotocol.hpp"
#include <eepp/ui/models/filesystemmodel.hpp> #include <eepp/ui/models/filesystemmodel.hpp>
#include <eepp/ui/uicodeeditor.hpp> #include <eepp/ui/uicodeeditor.hpp>
@@ -54,6 +55,8 @@ class Plugin : public UICodeEditorPlugin {
virtual void onFileSystemEvent( const FileEvent& ev, const FileInfo& file ); virtual void onFileSystemEvent( const FileEvent& ev, const FileInfo& file );
virtual FileSystemListenerOptions getFileSystemListenerOptions() const;
String i18n( const std::string& key, const String& def ) const; String i18n( const std::string& key, const String& def ) const;
UIIcon* findIcon( const std::string& iconName ); UIIcon* findIcon( const std::string& iconName );
+69 -18
View File
@@ -168,6 +168,10 @@ void PluginManager::setWorkspaceFolder( const std::string& workspaceFolder ) {
mWorkspaceFolder = workspaceFolder; mWorkspaceFolder = workspaceFolder;
json data{ { "folder", mWorkspaceFolder } }; json data{ { "folder", mWorkspaceFolder } };
sendBroadcast( PluginMessageType::WorkspaceFolderChanged, PluginMessageFormat::JSON, &data ); sendBroadcast( PluginMessageType::WorkspaceFolderChanged, PluginMessageFormat::JSON, &data );
// Workspace-scoped plugin filters are snapshots. Re-register after plugins
// receive the workspace notification and update their own paths.
unsubscribeFileSystemListener();
subscribeFileSystemListener();
} }
PluginRequestHandle PluginManager::sendRequest( PluginMessageType type, PluginMessageFormat format, PluginRequestHandle PluginManager::sendRequest( PluginMessageType type, PluginMessageFormat format,
@@ -295,6 +299,7 @@ void PluginManager::setMainSplitter( UISplitter* splitter ) {
void PluginManager::setFileSystemListener( FileSystemListener* listener ) { void PluginManager::setFileSystemListener( FileSystemListener* listener ) {
if ( listener == mFileSystemListener ) if ( listener == mFileSystemListener )
return; return;
unsubscribeFileSystemListener();
mFileSystemListener = listener; mFileSystemListener = listener;
sendBroadcast( PluginMessageType::FileSystemListenerReady, PluginMessageFormat::Empty, sendBroadcast( PluginMessageType::FileSystemListenerReady, PluginMessageFormat::Empty,
nullptr ); nullptr );
@@ -302,34 +307,80 @@ void PluginManager::setFileSystemListener( FileSystemListener* listener ) {
} }
void PluginManager::subscribeFileSystemListener( Plugin* plugin ) { void PluginManager::subscribeFileSystemListener( Plugin* plugin ) {
Lock l( mPluginsFSSubsMutex ); {
mPluginsFSSubs.insert( plugin ); Lock l( mPluginsFSSubsMutex );
if ( !mPluginsFSSubs.insert( plugin ).second )
return;
}
registerFileSystemListener( plugin );
} }
void PluginManager::unsubscribeFileSystemListener( Plugin* plugin ) { void PluginManager::unsubscribeFileSystemListener( Plugin* plugin ) {
Lock l( mPluginsFSSubsMutex ); Uint64 listenerId{ 0 };
mPluginsFSSubs.erase( plugin ); {
Lock l( mPluginsFSSubsMutex );
mPluginsFSSubs.erase( plugin );
auto it = mPluginFSListenerIds.find( plugin );
if ( it != mPluginFSListenerIds.end() ) {
listenerId = it->second;
mPluginFSListenerIds.erase( it );
}
}
if ( listenerId != 0 && mFileSystemListener )
mFileSystemListener->removeListener( listenerId );
} }
void PluginManager::subscribeFileSystemListener() { void PluginManager::subscribeFileSystemListener() {
if ( mFileSystemListenerCb != 0 || mFileSystemListener == nullptr ) if ( mFileSystemListener == nullptr )
return; return;
UnorderedSet<Plugin*> plugins;
mFileSystemListenerCb = {
mFileSystemListener->addListener( [this]( const FileEvent& ev, const FileInfo& file ) { Lock l( mPluginsFSSubsMutex );
UnorderedSet<Plugin*> plugins; plugins = mPluginsFSSubs;
{ }
Lock l( mPluginsFSSubsMutex ); for ( Plugin* plugin : plugins )
plugins = mPluginsFSSubs; registerFileSystemListener( plugin );
}
for ( Plugin* plugin : plugins )
plugin->onFileSystemEvent( ev, file );
} );
} }
void PluginManager::unsubscribeFileSystemListener() { void PluginManager::unsubscribeFileSystemListener() {
if ( mFileSystemListenerCb != 0 && mFileSystemListener ) std::vector<Uint64> listenerIds;
mFileSystemListener->removeListener( mFileSystemListenerCb ); {
Lock l( mPluginsFSSubsMutex );
listenerIds.reserve( mPluginFSListenerIds.size() );
for ( const auto& listener : mPluginFSListenerIds )
listenerIds.emplace_back( listener.second );
mPluginFSListenerIds.clear();
}
if ( mFileSystemListener ) {
for ( Uint64 listenerId : listenerIds )
mFileSystemListener->removeListener( listenerId );
}
}
void PluginManager::registerFileSystemListener( Plugin* plugin ) {
if ( mFileSystemListener == nullptr )
return;
{
Lock l( mPluginsFSSubsMutex );
if ( mPluginsFSSubs.find( plugin ) == mPluginsFSSubs.end() ||
mPluginFSListenerIds.find( plugin ) != mPluginFSListenerIds.end() )
return;
}
const Uint64 listenerId = mFileSystemListener->addListener(
[plugin]( const FileEvent& ev, const FileInfo& file ) {
plugin->onFileSystemEvent( ev, file );
},
plugin->getFileSystemListenerOptions() );
bool removeListener{ false };
{
Lock l( mPluginsFSSubsMutex );
if ( mPluginsFSSubs.find( plugin ) != mPluginsFSSubs.end() )
mPluginFSListenerIds[plugin] = listenerId;
else
removeListener = true;
}
if ( removeListener )
mFileSystemListener->removeListener( listenerId );
} }
void PluginManager::sendBroadcast( const PluginMessageType& notification, void PluginManager::sendBroadcast( const PluginMessageType& notification,
+3 -1
View File
@@ -393,8 +393,8 @@ class PluginManager {
Mutex mPluginsFSSubsMutex; Mutex mPluginsFSSubsMutex;
SubscribedPlugins mSubscribedPlugins; SubscribedPlugins mSubscribedPlugins;
OnLoadFileCb mLoadFileFn; OnLoadFileCb mLoadFileFn;
Uint64 mFileSystemListenerCb{ 0 };
UnorderedSet<Plugin*> mPluginsFSSubs; UnorderedSet<Plugin*> mPluginsFSSubs;
UnorderedMap<Plugin*, Uint64> mPluginFSListenerIds;
bool mClosing{ false }; bool mClosing{ false };
bool mPluginReloadEnabled{ false }; bool mPluginReloadEnabled{ false };
bool mPluginsDisabled{ false }; bool mPluginsDisabled{ false };
@@ -410,6 +410,8 @@ class PluginManager {
void subscribeFileSystemListener(); void subscribeFileSystemListener();
void unsubscribeFileSystemListener(); void unsubscribeFileSystemListener();
void registerFileSystemListener( Plugin* plugin );
}; };
class PluginsModel : public Model { class PluginsModel : public Model {
+23 -14
View File
@@ -245,6 +245,7 @@ std::shared_ptr<FileListModel>
ProjectDirectoryTree::asModel( const size_t& max, const std::vector<CommandInfo>& prependCommands, ProjectDirectoryTree::asModel( const size_t& max, const std::vector<CommandInfo>& prependCommands,
const std::string& basePath, const std::string& basePath,
const std::vector<std::string>& skipExtensions ) const { const std::vector<std::string>& skipExtensions ) const {
Lock rl( mMatchingMutex );
size_t namesSize = mNames.size(); size_t namesSize = mNames.size();
size_t rmax = eemin( namesSize, max ); size_t rmax = eemin( namesSize, max );
std::vector<std::string> files; std::vector<std::string> files;
@@ -440,6 +441,7 @@ void ProjectDirectoryTree::tryAddFile( const FileInfo& file ) {
} }
} }
if ( foundPattern ) { if ( foundPattern ) {
Lock rl( mMatchingMutex );
Lock l( mFilesMutex ); Lock l( mFilesMutex );
auto exists = auto exists =
std::find( mFiles.begin(), mFiles.end(), file.getFilepath() ) != mFiles.end(); std::find( mFiles.begin(), mFiles.end(), file.getFilepath() ) != mFiles.end();
@@ -457,6 +459,7 @@ void ProjectDirectoryTree::addFile( const FileInfo& file ) {
return; return;
if ( mIgnoreHidden && file.isHidden() ) if ( mIgnoreHidden && file.isHidden() )
return; return;
Lock rl( mMatchingMutex );
Lock l( mFilesMutex ); Lock l( mFilesMutex );
std::vector<std::string> files; std::vector<std::string> files;
std::vector<std::string> names; std::vector<std::string> names;
@@ -490,13 +493,15 @@ void ProjectDirectoryTree::addFile( const FileInfo& file ) {
} }
void ProjectDirectoryTree::moveFile( const FileInfo& file, const std::string& oldFilename ) { void ProjectDirectoryTree::moveFile( const FileInfo& file, const std::string& oldFilename ) {
Lock rl( mMatchingMutex );
Lock l( mFilesMutex ); Lock l( mFilesMutex );
if ( file.isDirectory() ) { if ( file.isDirectory() ) {
std::string dir( file.getDirectoryPath() ); std::string dir( file.getDirectoryPath() );
FileSystem::dirRemoveSlashAtEnd( dir ); FileSystem::dirRemoveSlashAtEnd( dir );
std::string parentDir( FileSystem::fileRemoveFileName( dir ) ); std::string parentDir( FileSystem::fileRemoveFileName( dir ) );
FileSystem::dirAddSlashAtEnd( parentDir ); FileSystem::dirAddSlashAtEnd( parentDir );
std::string oldDir( parentDir + oldFilename ); std::string oldDir( FileSystem::isRelativePath( oldFilename ) ? parentDir + oldFilename
: oldFilename );
FileSystem::dirAddSlashAtEnd( dir ); FileSystem::dirAddSlashAtEnd( dir );
FileSystem::dirAddSlashAtEnd( oldDir ); FileSystem::dirAddSlashAtEnd( oldDir );
std::vector<std::string> files; std::vector<std::string> files;
@@ -525,7 +530,9 @@ void ProjectDirectoryTree::moveFile( const FileInfo& file, const std::string& ol
} else { } else {
std::string dir( file.getDirectoryPath() ); std::string dir( file.getDirectoryPath() );
FileSystem::dirAddSlashAtEnd( dir ); FileSystem::dirAddSlashAtEnd( dir );
size_t index = findFileIndex( dir + oldFilename ); const std::string oldPath =
FileSystem::isRelativePath( oldFilename ) ? dir + oldFilename : oldFilename;
size_t index = findFileIndex( oldPath );
if ( index != std::string::npos ) { if ( index != std::string::npos ) {
IgnoreMatcherManager matcher( getIgnoreMatcherFromPath( file.getFilepath() ) ); IgnoreMatcherManager matcher( getIgnoreMatcherFromPath( file.getFilepath() ) );
if ( !( mIgnoreHidden && file.isHidden() ) && if ( !( mIgnoreHidden && file.isHidden() ) &&
@@ -554,19 +561,20 @@ void ProjectDirectoryTree::removeFile( const FileInfo& file ) {
} }
if ( wasDir ) { if ( wasDir ) {
std::vector<std::string> files;
std::vector<std::string> names;
files.reserve( mFiles.size() );
names.reserve( mNames.size() );
for ( size_t i = 0; i < mFiles.size(); i++ ) {
if ( !String::startsWith( mFiles[i], removedDir ) ) {
files.emplace_back( mFiles[i] );
names.emplace_back( mNames[i] );
}
}
{ {
Lock rl( mMatchingMutex );
Lock l( mFilesMutex ); Lock l( mFilesMutex );
std::vector<std::string> files;
std::vector<std::string> names;
files.reserve( mFiles.size() );
names.reserve( mNames.size() );
for ( size_t i = 0; i < mFiles.size(); i++ ) {
if ( !String::startsWith( mFiles[i], removedDir ) ) {
files.emplace_back( mFiles[i] );
names.emplace_back( mNames[i] );
}
}
mFiles = std::move( files ); mFiles = std::move( files );
mNames = std::move( names ); mNames = std::move( names );
} }
@@ -574,9 +582,10 @@ void ProjectDirectoryTree::removeFile( const FileInfo& file ) {
Lock ld2( mDirectoriesMutex ); Lock ld2( mDirectoriesMutex );
mDirectories.erase( std::find( mDirectories.begin(), mDirectories.end(), removedDir ) ); mDirectories.erase( std::find( mDirectories.begin(), mDirectories.end(), removedDir ) );
} else { } else {
Lock rl( mMatchingMutex );
Lock l( mFilesMutex );
size_t index = findFileIndex( file.getFilepath() ); size_t index = findFileIndex( file.getFilepath() );
if ( index != std::string::npos ) { if ( index != std::string::npos ) {
Lock l( mFilesMutex );
mFiles.erase( mFiles.begin() + index ); mFiles.erase( mFiles.begin() + index );
mNames.erase( mNames.begin() + index ); mNames.erase( mNames.begin() + index );
} }