mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-01 18:50:16 +03:00
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:
@@ -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
|
||||
@@ -5059,12 +5059,16 @@ void App::init( InitParameters& params ) {
|
||||
mFileWatcher->addWatch( mPidPath, mFileSystemListener );
|
||||
mFileWatcher->watch();
|
||||
mPluginManager->setFileSystemListener( mFileSystemListener );
|
||||
mIpcListenerId =
|
||||
mFileSystemListener->addListener( [this]( const FileEvent& fe, const FileInfo& fi ) {
|
||||
if ( !( ( fe.type == FileSystemEventType::Add ||
|
||||
fe.type == FileSystemEventType::Modified ) &&
|
||||
fe.directory == mPidPath ) )
|
||||
return;
|
||||
FileSystemListener::ListenerOptions ipcListenerOptions;
|
||||
FileSystemListenerFilter ipcListenerFilter;
|
||||
ipcListenerFilter.eventTypes =
|
||||
FileSystemListener::eventTypeMask( FileSystemEventType::Add ) |
|
||||
FileSystemListener::eventTypeMask( FileSystemEventType::Modified );
|
||||
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;
|
||||
FileSystem::fileGet( fi.getFilepath(), path );
|
||||
String::trimInPlace( path, ' ' );
|
||||
@@ -5091,7 +5095,8 @@ void App::init( InitParameters& params ) {
|
||||
} );
|
||||
}
|
||||
FileSystem::fileRemove( fi.getFilepath() );
|
||||
} );
|
||||
},
|
||||
std::move( ipcListenerOptions ) );
|
||||
#endif
|
||||
|
||||
mNotificationCenter = std::make_unique<NotificationCenter>(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "filesystemlistener.hpp"
|
||||
#include <eepp/core/small_vector.hpp>
|
||||
#include <eepp/scene/actionmanager.hpp>
|
||||
#include <eepp/scene/scenemanager.hpp>
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
@@ -8,6 +9,26 @@
|
||||
|
||||
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 ) {
|
||||
switch ( action ) {
|
||||
case FileSystemEventType::Add:
|
||||
@@ -45,6 +66,17 @@ FileSystemListener::~FileSystemListener() {
|
||||
// block instead of an atomic flag.
|
||||
eeASSERT( !Engine::existsSingleton() || Engine::isMainThread() );
|
||||
*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() )
|
||||
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,
|
||||
const std::string& filename, efsw::Action action,
|
||||
const std::string& oldFilename ) {
|
||||
// The whole logical event (model, directory tree, open documents,
|
||||
// callbacks) must run on the main thread in its original order: the model
|
||||
// update used to complete synchronously before the other consequences, and
|
||||
// all of them touch UI-owned state. The destructor cancels queued actions
|
||||
// (tagged with mEventActionTag), so a runnable can never outlive the
|
||||
// listener.
|
||||
if ( !Engine::isMainThread() ) {
|
||||
eeASSERT( !Engine::isMainThread() );
|
||||
FileEvent event( (FileSystemEventType)action, dir, filename, oldFilename );
|
||||
FileInfo file( ( endsWithSlash( dir ) ? dir : ( dir + FileSystem::getOSSlash() ) ) + filename,
|
||||
false );
|
||||
std::shared_ptr<ProjectDirectoryTree> dirTree;
|
||||
{
|
||||
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() ) {
|
||||
scene->runOnMainThread(
|
||||
[lifetime = mLifetime, this, dir, filename, action, oldFilename]() {
|
||||
if ( !lifetime->load( std::memory_order_acquire ) )
|
||||
return;
|
||||
handleFileAction( 0, dir, filename, action, oldFilename );
|
||||
[lifetime = mLifetime, this]() {
|
||||
if ( lifetime->load( std::memory_order_acquire ) )
|
||||
drainFileActions();
|
||||
},
|
||||
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 ) {
|
||||
case efsw::Actions::Add:
|
||||
case efsw::Actions::Delete:
|
||||
case efsw::Actions::Moved: {
|
||||
FileEvent event( (FileSystemEventType)action, dir, filename, oldFilename );
|
||||
|
||||
if ( Log::instance() && Log::instance()->getLogLevelThreshold() == LogLevel::Debug ) {
|
||||
std::string txt =
|
||||
"DIR ( " + event.directory + " ) FILE ( " +
|
||||
@@ -97,14 +172,12 @@ void FileSystemListener::handleFileAction( efsw::WatchID, const std::string& dir
|
||||
}
|
||||
|
||||
if ( mFileSystemModel )
|
||||
mFileSystemModel->handleFileEvent( event );
|
||||
|
||||
if ( mDirTree )
|
||||
mDirTree->onChange( (ProjectDirectoryTree::Action)action, file, oldFilename );
|
||||
mFileSystemModel->handleFileEvent( event, file );
|
||||
|
||||
if ( action == efsw::Actions::Moved ) {
|
||||
FileInfo oldFile( FileSystem::isRelativePath( oldFilename ) ? dir + oldFilename
|
||||
: oldFilename );
|
||||
FileInfo oldFile( FileSystem::isRelativePath( event.oldFilename )
|
||||
? event.directory + event.oldFilename
|
||||
: event.oldFilename );
|
||||
if ( file.isLink() )
|
||||
file = FileInfo( file.linksTo() );
|
||||
|
||||
@@ -125,11 +198,7 @@ void FileSystemListener::handleFileAction( efsw::WatchID, const std::string& dir
|
||||
if ( isFileOpen( file ) )
|
||||
notifyChange( file );
|
||||
|
||||
Lock l( mCbsMutex );
|
||||
if ( !mCbs.empty() ) {
|
||||
for ( const auto& cb : mCbs )
|
||||
cb.second( event, file );
|
||||
}
|
||||
dispatchCallbacks( ThreadAffinity::Main, event, file, eventPath );
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -139,35 +208,84 @@ void FileSystemListener::handleFileAction( efsw::WatchID, const std::string& dir
|
||||
if ( isFileOpen( file ) )
|
||||
notifyChange( file );
|
||||
|
||||
Lock l( mCbsMutex );
|
||||
if ( !mCbs.empty() ) {
|
||||
FileEvent event( (FileSystemEventType)action, dir, filename, oldFilename );
|
||||
for ( const auto& cb : mCbs )
|
||||
cb.second( event, file );
|
||||
}
|
||||
dispatchCallbacks( ThreadAffinity::Main, event, file, eventPath );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FileSystemListener::setDirTree( const std::shared_ptr<ProjectDirectoryTree>& dirTree ) {
|
||||
Lock l( mDirTreeMutex );
|
||||
mDirTree = dirTree;
|
||||
}
|
||||
|
||||
Uint64 FileSystemListener::addListener( const FileEventFn& fn ) {
|
||||
return addListener( fn, {} );
|
||||
}
|
||||
|
||||
Uint64 FileSystemListener::addListener( const FileEventFn& fn, ListenerOptions options ) {
|
||||
Lock l( mCbsMutex );
|
||||
Uint64 id = ++mLastId;
|
||||
mCbs[id] = fn;
|
||||
mCbs[id] = std::make_shared<Listener>( fn, std::move( options ) );
|
||||
return id;
|
||||
}
|
||||
|
||||
bool FileSystemListener::removeListener( const Uint64& id ) {
|
||||
Lock l( mCbsMutex );
|
||||
auto it = mCbs.find( id );
|
||||
if ( it != mCbs.end() ) {
|
||||
mCbs.erase( it );
|
||||
return true;
|
||||
void FileSystemListener::dispatchCallbacks( ThreadAffinity affinity, const FileEvent& event,
|
||||
const FileInfo& file, const std::string& eventPath,
|
||||
bool resolveLinks ) {
|
||||
SmallVector<std::shared_ptr<Listener>, 8> callbacks;
|
||||
{
|
||||
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 ) {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#ifndef ECODE_FILESYSTEMLISTENER_HPP
|
||||
#define ECODE_FILESYSTEMLISTENER_HPP
|
||||
|
||||
#include "boundedeventqueue.hpp"
|
||||
#include "filesystemlisteneroptions.hpp"
|
||||
#include "projectdirectorytree.hpp"
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <eepp/system/fileinfo.hpp>
|
||||
#include <eepp/ui/models/filesystemmodel.hpp>
|
||||
#include <eepp/ui/tools/uicodeeditorsplitter.hpp>
|
||||
@@ -17,9 +20,44 @@ using namespace EE::UI::Tools;
|
||||
|
||||
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 {
|
||||
public:
|
||||
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,
|
||||
std::shared_ptr<FileSystemModel> fileSystemModel,
|
||||
@@ -36,22 +74,49 @@ class FileSystemListener : public efsw::FileWatchListener {
|
||||
|
||||
Uint64 addListener( const FileEventFn& fn );
|
||||
|
||||
Uint64 addListener( const FileEventFn& fn, ListenerOptions options );
|
||||
|
||||
bool removeListener( const Uint64& id );
|
||||
|
||||
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;
|
||||
std::shared_ptr<FileSystemModel> mFileSystemModel;
|
||||
std::shared_ptr<ProjectDirectoryTree> mDirTree;
|
||||
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;
|
||||
Mutex mCbsMutex;
|
||||
Mutex mDirTreeMutex;
|
||||
BoundedEventQueue<PendingFileAction> mPendingEvents;
|
||||
// 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
|
||||
// dereference the destroyed listener.
|
||||
Uint64 mEventActionTag{ 0 };
|
||||
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 );
|
||||
|
||||
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 ) {
|
||||
registerSnippetLocatorProvider();
|
||||
Lock l( mDocMutex );
|
||||
|
||||
@@ -109,6 +109,7 @@ class AutoCompletePlugin : public Plugin {
|
||||
bool onMouseDoubleClick( UICodeEditor*, const Vector2i&, const Uint32& ) override;
|
||||
bool onMouseMove( UICodeEditor*, const Vector2i&, const Uint32& ) override;
|
||||
void onFileSystemEvent( const FileEvent&, const FileInfo& ) override;
|
||||
FileSystemListenerOptions getFileSystemListenerOptions() const override;
|
||||
void onLoadProject( const std::string& projectFolder,
|
||||
const std::string& projectStatePath ) override;
|
||||
|
||||
|
||||
@@ -562,6 +562,17 @@ void GitPlugin::onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) {
|
||||
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,
|
||||
const Vector2f& position ) {
|
||||
// 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;
|
||||
|
||||
FileSystemListenerOptions getFileSystemListenerOptions() const override;
|
||||
|
||||
void onRegister( UICodeEditor* ) override;
|
||||
|
||||
void onUnregister( UICodeEditor* ) override;
|
||||
|
||||
@@ -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 ) {
|
||||
if ( mReady ) {
|
||||
if ( loadTime != Time::Zero ) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef ECODE_PLUGIN_HPP
|
||||
#define ECODE_PLUGIN_HPP
|
||||
|
||||
#include "../filesystemlisteneroptions.hpp"
|
||||
#include "lsp/lspprotocol.hpp"
|
||||
#include <eepp/ui/models/filesystemmodel.hpp>
|
||||
#include <eepp/ui/uicodeeditor.hpp>
|
||||
@@ -54,6 +55,8 @@ class Plugin : public UICodeEditorPlugin {
|
||||
|
||||
virtual void onFileSystemEvent( const FileEvent& ev, const FileInfo& file );
|
||||
|
||||
virtual FileSystemListenerOptions getFileSystemListenerOptions() const;
|
||||
|
||||
String i18n( const std::string& key, const String& def ) const;
|
||||
|
||||
UIIcon* findIcon( const std::string& iconName );
|
||||
|
||||
@@ -168,6 +168,10 @@ void PluginManager::setWorkspaceFolder( const std::string& workspaceFolder ) {
|
||||
mWorkspaceFolder = workspaceFolder;
|
||||
json data{ { "folder", mWorkspaceFolder } };
|
||||
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,
|
||||
@@ -295,6 +299,7 @@ void PluginManager::setMainSplitter( UISplitter* splitter ) {
|
||||
void PluginManager::setFileSystemListener( FileSystemListener* listener ) {
|
||||
if ( listener == mFileSystemListener )
|
||||
return;
|
||||
unsubscribeFileSystemListener();
|
||||
mFileSystemListener = listener;
|
||||
sendBroadcast( PluginMessageType::FileSystemListenerReady, PluginMessageFormat::Empty,
|
||||
nullptr );
|
||||
@@ -302,34 +307,80 @@ void PluginManager::setFileSystemListener( FileSystemListener* listener ) {
|
||||
}
|
||||
|
||||
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 ) {
|
||||
Lock l( mPluginsFSSubsMutex );
|
||||
mPluginsFSSubs.erase( plugin );
|
||||
Uint64 listenerId{ 0 };
|
||||
{
|
||||
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() {
|
||||
if ( mFileSystemListenerCb != 0 || mFileSystemListener == nullptr )
|
||||
if ( mFileSystemListener == nullptr )
|
||||
return;
|
||||
|
||||
mFileSystemListenerCb =
|
||||
mFileSystemListener->addListener( [this]( const FileEvent& ev, const FileInfo& file ) {
|
||||
UnorderedSet<Plugin*> plugins;
|
||||
{
|
||||
Lock l( mPluginsFSSubsMutex );
|
||||
plugins = mPluginsFSSubs;
|
||||
}
|
||||
for ( Plugin* plugin : plugins )
|
||||
plugin->onFileSystemEvent( ev, file );
|
||||
} );
|
||||
UnorderedSet<Plugin*> plugins;
|
||||
{
|
||||
Lock l( mPluginsFSSubsMutex );
|
||||
plugins = mPluginsFSSubs;
|
||||
}
|
||||
for ( Plugin* plugin : plugins )
|
||||
registerFileSystemListener( plugin );
|
||||
}
|
||||
|
||||
void PluginManager::unsubscribeFileSystemListener() {
|
||||
if ( mFileSystemListenerCb != 0 && mFileSystemListener )
|
||||
mFileSystemListener->removeListener( mFileSystemListenerCb );
|
||||
std::vector<Uint64> listenerIds;
|
||||
{
|
||||
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,
|
||||
|
||||
@@ -393,8 +393,8 @@ class PluginManager {
|
||||
Mutex mPluginsFSSubsMutex;
|
||||
SubscribedPlugins mSubscribedPlugins;
|
||||
OnLoadFileCb mLoadFileFn;
|
||||
Uint64 mFileSystemListenerCb{ 0 };
|
||||
UnorderedSet<Plugin*> mPluginsFSSubs;
|
||||
UnorderedMap<Plugin*, Uint64> mPluginFSListenerIds;
|
||||
bool mClosing{ false };
|
||||
bool mPluginReloadEnabled{ false };
|
||||
bool mPluginsDisabled{ false };
|
||||
@@ -410,6 +410,8 @@ class PluginManager {
|
||||
void subscribeFileSystemListener();
|
||||
|
||||
void unsubscribeFileSystemListener();
|
||||
|
||||
void registerFileSystemListener( Plugin* plugin );
|
||||
};
|
||||
|
||||
class PluginsModel : public Model {
|
||||
|
||||
@@ -245,6 +245,7 @@ std::shared_ptr<FileListModel>
|
||||
ProjectDirectoryTree::asModel( const size_t& max, const std::vector<CommandInfo>& prependCommands,
|
||||
const std::string& basePath,
|
||||
const std::vector<std::string>& skipExtensions ) const {
|
||||
Lock rl( mMatchingMutex );
|
||||
size_t namesSize = mNames.size();
|
||||
size_t rmax = eemin( namesSize, max );
|
||||
std::vector<std::string> files;
|
||||
@@ -440,6 +441,7 @@ void ProjectDirectoryTree::tryAddFile( const FileInfo& file ) {
|
||||
}
|
||||
}
|
||||
if ( foundPattern ) {
|
||||
Lock rl( mMatchingMutex );
|
||||
Lock l( mFilesMutex );
|
||||
auto exists =
|
||||
std::find( mFiles.begin(), mFiles.end(), file.getFilepath() ) != mFiles.end();
|
||||
@@ -457,6 +459,7 @@ void ProjectDirectoryTree::addFile( const FileInfo& file ) {
|
||||
return;
|
||||
if ( mIgnoreHidden && file.isHidden() )
|
||||
return;
|
||||
Lock rl( mMatchingMutex );
|
||||
Lock l( mFilesMutex );
|
||||
std::vector<std::string> files;
|
||||
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 ) {
|
||||
Lock rl( mMatchingMutex );
|
||||
Lock l( mFilesMutex );
|
||||
if ( file.isDirectory() ) {
|
||||
std::string dir( file.getDirectoryPath() );
|
||||
FileSystem::dirRemoveSlashAtEnd( dir );
|
||||
std::string parentDir( FileSystem::fileRemoveFileName( dir ) );
|
||||
FileSystem::dirAddSlashAtEnd( parentDir );
|
||||
std::string oldDir( parentDir + oldFilename );
|
||||
std::string oldDir( FileSystem::isRelativePath( oldFilename ) ? parentDir + oldFilename
|
||||
: oldFilename );
|
||||
FileSystem::dirAddSlashAtEnd( dir );
|
||||
FileSystem::dirAddSlashAtEnd( oldDir );
|
||||
std::vector<std::string> files;
|
||||
@@ -525,7 +530,9 @@ void ProjectDirectoryTree::moveFile( const FileInfo& file, const std::string& ol
|
||||
} else {
|
||||
std::string dir( file.getDirectoryPath() );
|
||||
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 ) {
|
||||
IgnoreMatcherManager matcher( getIgnoreMatcherFromPath( file.getFilepath() ) );
|
||||
if ( !( mIgnoreHidden && file.isHidden() ) &&
|
||||
@@ -554,19 +561,20 @@ void ProjectDirectoryTree::removeFile( const FileInfo& file ) {
|
||||
}
|
||||
|
||||
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 );
|
||||
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 );
|
||||
mNames = std::move( names );
|
||||
}
|
||||
@@ -574,9 +582,10 @@ void ProjectDirectoryTree::removeFile( const FileInfo& file ) {
|
||||
Lock ld2( mDirectoriesMutex );
|
||||
mDirectories.erase( std::find( mDirectories.begin(), mDirectories.end(), removedDir ) );
|
||||
} else {
|
||||
Lock rl( mMatchingMutex );
|
||||
Lock l( mFilesMutex );
|
||||
size_t index = findFileIndex( file.getFilepath() );
|
||||
if ( index != std::string::npos ) {
|
||||
Lock l( mFilesMutex );
|
||||
mFiles.erase( mFiles.begin() + index );
|
||||
mNames.erase( mNames.begin() + index );
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user