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
+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->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>(
+160 -42
View File
@@ -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 ) {
+66 -1
View 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;
+11
View File
@@ -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;
+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 ) {
if ( mReady ) {
if ( loadTime != Time::Zero ) {
+3
View File
@@ -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 );
+69 -18
View File
@@ -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,
+3 -1
View File
@@ -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 {
+23 -14
View File
@@ -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 );
}