ecode: separate plugin detachment from destruction

Add an explicit Plugin::shutdown() phase that removes all application-facing
state while the complete plugin type is still alive. Detach editor and
document commands, listeners, clients, UI objects, and queued callbacks on
the main thread before scheduling resource-only destruction on a worker.

Move plugin-specific cleanup into unregisterEditors() and the PluginBase
unregister hooks. Keep potentially blocking cleanup, such as stopping LSP
servers or joining workers, in destructors so normal plugin unload does not
stall the UI.

Stop plugin reloads, broadcasts, late ready callbacks, and unlocked command
dispatch once application closing begins. During final shutdown, unload
plugins synchronously and wait for any previously queued destruction.

Document the Plugin and PluginBase lifecycle, cleanup checklist, and a
complete implementation example.

Previously, plugin destructors could mutate editor and UI state from a worker
thread, while destructor-time virtual dispatch skipped derived unregister
logic. This could leave commands capturing a destroyed plugin instance or
race registrations from a newly loaded generation. A queued key event during
application close could then execute one of these stale callbacks and jump to
invalid memory.
This commit is contained in:
Martín Lucas Golini
2026-09-16 01:53:22 -03:00
parent 4e3c2d966f
commit f829f18920
31 changed files with 448 additions and 200 deletions
+25 -3
View File
@@ -115,6 +115,8 @@ bool App::isAnyTerminalDirty() const {
}
bool App::onCloseRequestCallback( EE::Window::Window* ) {
if ( mClosing )
return true;
if ( mSplitter->isAnyEditorDirty() &&
( !mConfig.workspace.sessionSnapshot || mCurrentProject.empty() ) ) {
if ( mCloseMsgBox )
@@ -127,6 +129,7 @@ bool App::onCloseRequestCallback( EE::Window::Window* ) {
mCloseMsgBox->on( Event::OnConfirm, [this]( const Event* ) {
saveProject();
saveConfig();
beginClosing();
mWindow->close();
} );
mCloseMsgBox->on( Event::OnWindowClose, [this]( auto ) { mCloseMsgBox = nullptr; } );
@@ -147,6 +150,7 @@ bool App::onCloseRequestCallback( EE::Window::Window* ) {
mCloseMsgBox->on( Event::OnConfirm, [this]( const Event* ) {
saveProject();
saveConfig();
beginClosing();
mWindow->close();
} );
mCloseMsgBox->on( Event::OnWindowClose, [this]( auto ) { mCloseMsgBox = nullptr; } );
@@ -158,10 +162,19 @@ bool App::onCloseRequestCallback( EE::Window::Window* ) {
} else {
saveProject();
saveConfig();
beginClosing();
return true;
}
}
void App::beginClosing() {
if ( mClosing )
return;
mClosing = true;
if ( mPluginManager )
mPluginManager->beginShutdown();
}
void App::saveDoc() {
if ( !mSplitter->curEditorExistsAndFocused() )
return;
@@ -756,9 +769,16 @@ void App::initPluginManager() {
onPluginEnabled( plugin );
} else {
// If plugin loads asynchronously and is not ready, delay the plugin enabled callback
plugin->addOnReadyCallback( [this]( UICodeEditorPlugin* plugin, const Uint32& cbId ) {
mLifetime.weakHandle().run( [plugin]( App* app ) {
app->onPluginEnabled( static_cast<Plugin*>( plugin ) );
const std::string pluginId( plugin->getId() );
plugin->addOnReadyCallback( [lifetime = mLifetime.weakHandle(), pluginId](
UICodeEditorPlugin* plugin, const Uint32& cbId ) {
Plugin* readyPlugin = static_cast<Plugin*>( plugin );
lifetime.run( [pluginId, readyPlugin]( App* app ) {
Plugin* currentPlugin =
app->mPluginManager ? app->mPluginManager->get( pluginId ) : nullptr;
if ( currentPlugin == readyPlugin && !app->mPluginManager->isClosing() &&
currentPlugin->isReady() )
app->onPluginEnabled( currentPlugin );
} );
plugin->removeReadyCallback( cbId );
} );
@@ -911,6 +931,8 @@ std::shared_ptr<ThreadPool> App::getThreadPool() const {
}
bool App::trySendUnlockedCmd( const KeyEvent& keyEvent ) {
if ( mClosing || !mWindow || !mWindow->isRunning() )
return false;
if ( mSplitter->curEditorExistsAndFocused() ) {
std::string cmd = mSplitter->getCurEditor()->getKeyBindings().getCommandFromKeyBind(
{ keyEvent.getKeyCode(), keyEvent.getMod() } );
+3
View File
@@ -740,6 +740,7 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
bool mFirstInstance{ false };
bool mPortableMode{ false };
bool mPortableModeFailed{ false };
bool mClosing{ false };
bool mDestroyingApp{ false };
Time mFrameTime{ Time::Zero };
bool mIncognito{ false };
@@ -831,6 +832,8 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
bool onCloseRequestCallback( EE::Window::Window* );
void beginClosing();
void addRemainingTabWidgets( Node* widget );
void updateEditorState();
@@ -269,12 +269,21 @@ AIAssistantPlugin::AIAssistantPlugin( PluginManager* pluginManager, bool sync )
}
AIAssistantPlugin::~AIAssistantPlugin() {
{
std::lock_guard<std::mutex> lock( mModelCatalogMutex );
mModelCatalog.reset();
}
waitUntilLoaded();
mShuttingDown = true;
}
void AIAssistantPlugin::unregisterEditors() {
{
std::lock_guard<std::mutex> lock( mModelCatalogMutex );
mModelCatalogCancelled->store( true );
if ( mModelCatalog )
mModelCatalog->cancel();
mModelCatalog.reset();
}
if ( SceneManager::existsSingleton() && !SceneManager::instance()->isShuttingDown() &&
getPluginContext() && getPluginContext()->getSplitter() ) {
@@ -284,15 +293,17 @@ AIAssistantPlugin::~AIAssistantPlugin() {
chat->setManager( nullptr );
} );
}
waitUntilLoaded();
mShuttingDown = true;
unsubscribeFileSystemListener();
if ( getPluginContext() && getPluginContext()->getMainLayout() ) {
getPluginContext()->getMainLayout()->unsetCommand( "new-ai-assistant" );
getPluginContext()->getMainLayout()->getKeyBindings().removeCommandKeybind(
"new-ai-assistant" );
}
PluginBase::unregisterEditors();
if ( mAIChatButton ) {
if ( mAIChatButtonPosCbId )
mAIChatButton->getParent()->removeEventListener( mAIChatButtonPosCbId );
mAIChatButton->close();
mAIChatButton = nullptr;
}
getPluginContext()->getConfig().removeTabWidgetType( "llm_chatui" );
}
@@ -93,6 +93,8 @@ class AIAssistantPlugin : public PluginBase {
void onRegisterDocument( TextDocument* doc ) override;
void unregisterEditors() override;
void initUI();
void displayBrokenUserConfigFileWarning();
@@ -316,27 +316,10 @@ AutoCompletePlugin::AutoCompletePlugin( PluginManager* pluginManager, bool sync
AutoCompletePlugin::~AutoCompletePlugin() {
waitUntilLoaded();
mShuttingDown = true;
unregisterSnippetLocatorProvider();
mManager->unsubscribeMessages( this );
unsubscribeFileSystemListener();
while ( mSnippetJobs > 0 )
Sys::sleep( Milliseconds( 1 ) );
for ( auto& client : mSnippetClients )
client.second->detach();
mSnippetClients.clear();
mSnippetSessions.clear();
{
Lock l( mDocMutex );
Lock l2( mLangSymbolsMutex );
Lock l3( mSuggestionsMutex );
for ( const auto& editor : mEditors ) {
for ( auto listener : editor.second )
editor.first->removeEventListener( listener );
editor.first->unregisterPlugin( this );
}
}
bool isUpdating = false;
do {
{
@@ -349,6 +332,15 @@ AutoCompletePlugin::~AutoCompletePlugin() {
} while ( isUpdating );
}
void AutoCompletePlugin::unregisterEditors() {
unregisterSnippetLocatorProvider();
while ( !mEditors.empty() )
mEditors.begin()->first->unregisterPlugin( this );
for ( auto& client : mSnippetClients )
client.second->detach();
mSnippetClients.clear();
}
void AutoCompletePlugin::load( PluginManager* pluginManager ) {
Clock clock;
AtomicBoolScopedOp loading( mLoading, true );
@@ -834,7 +826,7 @@ void AutoCompletePlugin::onRegister( UICodeEditor* editor ) {
}
void AutoCompletePlugin::onUnregister( UICodeEditor* editor ) {
if ( mShuttingDown )
if ( mShuttingDown && !mUnregistering )
return;
if ( mSuggestionsEditor == editor )
resetSuggestions( editor );
@@ -854,6 +846,7 @@ void AutoCompletePlugin::onUnregister( UICodeEditor* editor ) {
for ( auto ceditor : mEditorDocs )
if ( ceditor.second == doc )
return;
doc->removeCommand( "autocomplete-from-current-doc-symbols" );
detachSnippetClient( doc );
mDocs.erase( doc );
mDocCache.erase( doc );
@@ -280,6 +280,8 @@ class AutoCompletePlugin : public Plugin {
void load( PluginManager* pluginManager );
void unregisterEditors() override;
void resetSuggestions( UICodeEditor* editor );
void updateSuggestions( const std::string& symbol, UICodeEditor* editor );
@@ -164,46 +164,11 @@ DebuggerPlugin::DebuggerPlugin( PluginManager* pluginManager, bool sync ) :
}
DebuggerPlugin::~DebuggerPlugin() {
mLifetime.invalidate();
waitUntilLoaded();
mShuttingDown = true;
{
Lock l( mClientsMutex );
for ( const auto& client : mClients )
client.first->unregisterClient( client.second.get() );
}
if ( mSidePanel && mTab ) {
if ( Engine::isMainThread() )
mSidePanel->removeTab( mTab );
else {
auto sidePanel = mSidePanel;
auto tab = mTab;
mSidePanel->runOnMainThread( [sidePanel, tab] { sidePanel->removeTab( tab ); } );
}
}
if ( getPluginContext()->getStatusBar() )
getPluginContext()->getStatusBar()->removeStatusBarElement( "status_app_debugger" );
mManager->unsubscribeMessages( this );
for ( auto editor : mEditors ) {
onBeforeUnregister( editor.first );
onUnregisterEditor( editor.first );
}
mDebugger.reset();
mListener.reset();
if ( SceneManager::existsSingleton() && !SceneManager::instance()->isShuttingDown() &&
getPluginContext() && getPluginContext()->getMainLayout() ) {
getPluginContext()->getMainLayout()->unsetCommands( mRegisteredCommands );
for ( const auto& kb : mKeyBindings )
getPluginContext()->getMainLayout()->getKeyBindings().removeCommandKeybind( kb.first );
}
}
void DebuggerPlugin::onSaveState( IniFile* state ) {
@@ -1578,8 +1543,37 @@ void DebuggerPlugin::onRegisterDocument( TextDocument* doc ) {
void DebuggerPlugin::onUnregisterDocument( TextDocument* doc ) {
Lock l( mClientsMutex );
doc->unregisterClient( mClients[doc].get() );
mClients.erase( doc );
auto client = mClients.find( doc );
if ( client != mClients.end() ) {
doc->unregisterClient( client->second.get() );
mClients.erase( client );
}
PluginBase::onUnregisterDocument( doc );
doc->removeCommand( "show-debugger-tab" );
}
void DebuggerPlugin::unregisterEditors() {
mLifetime.invalidate();
PluginBase::unregisterEditors();
{
Lock l( mClientsMutex );
for ( const auto& client : mClients )
client.first->unregisterClient( client.second.get() );
mClients.clear();
}
if ( mSidePanel && mTab ) {
mSidePanel->removeTab( mTab );
mTab = nullptr;
}
if ( getPluginContext()->getStatusBar() )
getPluginContext()->getStatusBar()->removeStatusBarElement( "status_app_debugger" );
if ( SceneManager::existsSingleton() && !SceneManager::instance()->isShuttingDown() &&
getPluginContext() && getPluginContext()->getMainLayout() ) {
getPluginContext()->getMainLayout()->unsetCommands( mRegisteredCommands );
for ( const auto& kb : mKeyBindings )
getPluginContext()->getMainLayout()->getKeyBindings().removeCommandKeybind( kb.first );
}
}
void DebuggerPlugin::onRegisterEditor( UICodeEditor* editor ) {
@@ -310,6 +310,8 @@ class DebuggerPlugin : public PluginBase {
virtual void onUnregisterDocument( TextDocument* doc ) override;
void unregisterEditors() override;
void onDocumentLineMove( TextDocument* doc, const Int64& fromLine, const Int64& toLine,
const Int64& numLines );
@@ -62,11 +62,6 @@ DiscordRPCplugin::~DiscordRPCplugin() {
if ( mIPC.isConnected() )
mIPC.clearActivity();
mShuttingDown = true;
for ( auto editor : mEditors ) {
onBeforeUnregister( editor.first );
onUnregisterEditor( editor.first );
}
}
void DiscordRPCplugin::load( PluginManager* pluginManager ) {
@@ -220,6 +215,11 @@ void DiscordRPCplugin::onUnregisterEditor( UICodeEditor* editor ) {
editor->removeActionsByTag( DebounceUniqueId );
}
void DiscordRPCplugin::onUnregisterDocument( TextDocument* doc ) {
doc->removeCommand( "discordrpc-reconnect" );
PluginBase::onUnregisterDocument( doc );
}
void DiscordRPCplugin::updateActivity( DiscordIPCActivity& a ) {
Lock l( mDataMutex );
Log::debug( "dcIPC: Activity in new file. lang = %s", mLastLang );
@@ -71,6 +71,8 @@ class DiscordRPCplugin : public PluginBase {
virtual void onUnregisterEditor( UICodeEditor* editor ) override;
virtual void onUnregisterDocument( TextDocument* doc ) override;
DiscordRPCplugin( PluginManager* pluginManager, bool sync );
void loadDiscordRPCConfig( const std::string& path, bool updateConfigFile );
@@ -48,23 +48,16 @@ FormatterPlugin::FormatterPlugin( PluginManager* pluginManager, bool sync ) :
FormatterPlugin::~FormatterPlugin() {
waitUntilLoaded();
mShuttingDown = true;
unsubscribeFileSystemListener();
if ( mWorkersCount != 0 ) {
std::unique_lock<std::mutex> lock( mWorkMutex );
mWorkerCondition.wait( lock, [this]() { return mWorkersCount <= 0; } );
}
}
for ( auto& editor : mEditors ) {
for ( auto& kb : mKeyBindings ) {
editor.first->getKeyBindings().removeCommandKeybind( kb.first );
if ( editor.first->hasDocument() )
editor.first->getDocument().removeCommand( kb.first );
}
for ( auto listener : editor.second )
editor.first->removeEventListener( listener );
editor.first->unregisterPlugin( this );
}
void FormatterPlugin::unregisterEditors() {
while ( !mEditors.empty() )
mEditors.begin()->first->unregisterPlugin( this );
}
void FormatterPlugin::onRegister( UICodeEditor* editor ) {
@@ -115,8 +108,10 @@ void FormatterPlugin::onUnregister( UICodeEditor* editor ) {
auto cbs = mEditors[editor];
for ( auto listener : cbs )
editor->removeEventListener( listener );
for ( auto& kb : mKeyBindings )
editor->getKeyBindings().removeCommandKeybind( kb.first );
if ( mShuttingDown )
if ( mShuttingDown && !mUnregistering )
return;
mEditors.erase( editor );
mEditorDocs.erase( editor );
@@ -126,11 +121,8 @@ void FormatterPlugin::onUnregister( UICodeEditor* editor ) {
if ( editorIt.second == doc )
return;
for ( auto& kb : mKeyBindings ) {
editor->getKeyBindings().removeCommandKeybind( kb.first );
if ( editor->hasDocument() )
editor->getDocument().removeCommand( kb.first );
}
for ( auto& kb : mKeyBindings )
doc->removeCommand( kb.first );
}
bool FormatterPlugin::getAutoFormatOnSave() const {
@@ -101,6 +101,8 @@ class FormatterPlugin : public Plugin {
void load( PluginManager* pluginManager );
virtual void unregisterEditors();
void loadFormatterConfig( const std::string& path, bool updateConfigFile );
void formatDoc( UICodeEditor* editor );
+34 -19
View File
@@ -210,27 +210,8 @@ GitPlugin::GitPlugin( PluginManager* pluginManager, bool sync ) :
}
GitPlugin::~GitPlugin() {
mLifetime.invalidate();
waitUntilLoaded();
mShuttingDown = true;
mCommitDetails.cancelDiffPreparation();
mDetachedHistory.details.cancelDiffPreparation();
mConflictViewCloseConnection.disconnect();
mConflictView = nullptr;
mConflictSessions.clear();
if ( mStatusButton )
mStatusButton->close();
if ( mSidePanel && mTab )
mSidePanel->removeTab( mTab );
endModelStyler();
if ( getUISceneNode() )
getUISceneNode()->removeActionsByTag( GIT_STATUS_UPDATE_TAG );
if ( mStatusBar && mRepositionCbId )
mStatusBar->removeEventListener( mRepositionCbId );
{
Lock l( mGitBranchMutex );
@@ -259,6 +240,31 @@ GitPlugin::~GitPlugin() {
Sys::sleep( Milliseconds( 1.f ) );
}
void GitPlugin::unregisterEditors() {
mLifetime.invalidate();
mCommitDetails.cancelDiffPreparation();
mDetachedHistory.details.cancelDiffPreparation();
mConflictViewCloseConnection.disconnect();
mConflictView = nullptr;
mConflictSessions.clear();
if ( mStatusButton ) {
mStatusButton->close();
mStatusButton = nullptr;
}
if ( mSidePanel && mTab ) {
mSidePanel->removeTab( mTab );
mTab = nullptr;
}
endModelStyler();
if ( getUISceneNode() )
getUISceneNode()->removeActionsByTag( GIT_STATUS_UPDATE_TAG );
if ( mStatusBar && mRepositionCbId ) {
mStatusBar->removeEventListener( mRepositionCbId );
mRepositionCbId = 0;
}
PluginBase::unregisterEditors();
}
void GitPlugin::onSaveState( IniFile* state ) {
std::string commitMessage;
Base64::encode( mLastCommitMsg.toUtf8(), commitMessage );
@@ -2413,7 +2419,16 @@ void GitPlugin::onRegister( UICodeEditor* editor ) {
}
void GitPlugin::onUnregister( UICodeEditor* editor ) {
TextDocument* doc = editor->getDocumentRef().get();
PluginBase::onUnregister( editor );
if ( mDocs.find( doc ) == mDocs.end() ) {
doc->removeCommand( "show-source-control-tab" );
doc->removeCommand( "git-pull" );
doc->removeCommand( "git-push" );
doc->removeCommand( "git-fetch" );
doc->removeCommand( "git-commit" );
doc->removeCommand( "git-show-history" );
}
}
bool GitPlugin::onCreateContextMenu( UICodeEditor*, UIPopUpMenu* menu, const Vector2i& /*position*/,
@@ -97,6 +97,8 @@ class GitPlugin : public PluginBase {
void onUnregister( UICodeEditor* ) override;
void unregisterEditors() override;
bool onCreateContextMenu( UICodeEditor* editor, UIPopUpMenu* menu, const Vector2i& position,
const Uint32& flags ) override;
+10 -18
View File
@@ -85,8 +85,6 @@ LinterPlugin::LinterPlugin( PluginManager* pluginManager, bool sync ) : Plugin(
LinterPlugin::~LinterPlugin() {
waitUntilLoaded();
mShuttingDown = true;
mManager->unsubscribeMessages( this );
unsubscribeFileSystemListener();
{
std::lock_guard l( mRunningProcessesMutex );
@@ -98,17 +96,11 @@ LinterPlugin::~LinterPlugin() {
std::unique_lock<std::mutex> lock( mWorkMutex );
mWorkerCondition.wait( lock, [this]() { return mWorkersCount <= 0; } );
}
for ( const auto& editor : mEditors ) {
for ( auto& kb : mKeyBindings ) {
editor.first->getKeyBindings().removeCommandKeybind( kb.first );
if ( editor.first->hasDocument() )
editor.first->getDocument().removeCommand( kb.first );
}
for ( auto listener : editor.second )
editor.first->removeEventListener( listener );
editor.first->unregisterPlugin( this );
}
void LinterPlugin::unregisterEditors() {
while ( !mEditors.empty() )
mEditors.begin()->first->unregisterPlugin( this );
}
size_t LinterPlugin::linterFilePatternPosition( const std::vector<std::string>& patterns ) {
@@ -753,7 +745,7 @@ void LinterPlugin::onRegister( UICodeEditor* editor ) {
}
void LinterPlugin::onUnregister( UICodeEditor* editor ) {
if ( mShuttingDown )
if ( mShuttingDown && !mUnregistering )
return;
Lock l( mDocMutex );
@@ -761,17 +753,17 @@ void LinterPlugin::onUnregister( UICodeEditor* editor ) {
auto cbs = mEditors[editor];
for ( auto listener : cbs )
editor->removeEventListener( listener );
for ( auto& kb : mKeyBindings )
editor->getKeyBindings().removeCommandKeybind( kb.first );
mEditors.erase( editor );
mEditorDocs.erase( editor );
for ( auto editorIt : mEditorDocs )
if ( editorIt.second == doc )
return;
for ( auto& kb : mKeyBindings ) {
editor->getKeyBindings().removeCommandKeybind( kb.first );
if ( editor->hasDocument() )
editor->getDocument().removeCommand( kb.first );
}
for ( auto& kb : mKeyBindings )
doc->removeCommand( kb.first );
doc->removeCommand( "linter-copy-error-message" );
mDocs.erase( doc );
mDirtyDoc.erase( doc );
@@ -170,6 +170,8 @@ class LinterPlugin : public Plugin {
void load( PluginManager* pluginManager );
virtual void unregisterEditors();
void lintDoc( std::shared_ptr<TextDocument> doc );
void runLinter( std::shared_ptr<TextDocument> doc, const Linter& linter,
+32 -42
View File
@@ -78,6 +78,23 @@ static Action::UniqueID getMouseMoveHash( UICodeEditor* editor ) {
reinterpret_cast<Action::UniqueID>( editor ) );
}
static constexpr const char* LSPDocumentCommands[] = {
"lsp-go-to-definition",
"lsp-go-to-declaration",
"lsp-go-to-implementation",
"lsp-go-to-type-definition",
"lsp-switch-header-source",
"lsp-symbol-info",
"lsp-symbol-references",
"lsp-memory-usage",
"lsp-symbol-code-action",
"lsp-rename-symbol-under-cursor",
"lsp-refresh-semantic-highlighting",
"lsp-format-range",
"lsp-plugin-restart",
"lsp-show-document-symbols",
};
static json getURIAndPositionJSON( UICodeEditor* editor ) {
json data;
auto doc = editor->getDocumentRef();
@@ -292,31 +309,15 @@ LSPClientPlugin::LSPClientPlugin( PluginManager* pluginManager, bool sync ) :
}
LSPClientPlugin::~LSPClientPlugin() {
mLifetime.invalidate();
waitUntilLoaded();
mShuttingDown = true;
mManager->unsubscribeMessages( this );
unsubscribeFileSystemListener();
{
Lock l( mDocMutex );
for ( const auto& editor : mEditors ) {
UICodeEditor* codeEditor = editor.first;
for ( auto& kb : mKeyBindings ) {
codeEditor->getKeyBindings().removeCommandKeybind( kb.first );
if ( codeEditor->hasDocument() )
codeEditor->getDocument().removeCommand( kb.first );
}
for ( auto listener : editor.second )
codeEditor->removeEventListener( listener );
if ( mBreadcrumb )
codeEditor->unregisterTopSpace( this );
codeEditor->unregisterPlugin( this );
if ( mManager->getSplitter()->editorExists( codeEditor ) )
codeEditor->removeActionsByTag( getMouseMoveHash( codeEditor ) );
}
if ( nullptr == mManager->getSplitter() )
return;
}
}
void LSPClientPlugin::unregisterEditors() {
mLifetime.invalidate();
while ( !mEditors.empty() )
mEditors.begin()->first->unregisterPlugin( this );
mClientManager.detachDocuments();
}
void LSPClientPlugin::update( UICodeEditor* ) {
@@ -1157,21 +1158,7 @@ void LSPClientPlugin::loadLSPConfig( std::vector<LSPDefinition>& lsps, const std
if ( j.contains( "keybindings" ) ) {
auto& kb = j["keybindings"];
auto list = { "lsp-go-to-definition",
"lsp-go-to-declaration",
"lsp-go-to-implementation",
"lsp-go-to-type-definition",
"lsp-switch-header-source",
"lsp-symbol-info",
"lsp-symbol-references",
"lsp-memory-usage",
"lsp-symbol-code-action",
"lsp-rename-symbol-under-cursor",
"lsp-refresh-semantic-highlighting",
"lsp-format-range",
"lsp-plugin-restart",
"lsp-show-document-symbols" };
for ( const auto& key : list ) {
for ( const auto* key : LSPDocumentCommands ) {
if ( kb.contains( key ) ) {
if ( !kb[key].empty() )
mKeyBindings[key] = kb[key];
@@ -1525,7 +1512,10 @@ void LSPClientPlugin::onRegister( UICodeEditor* editor ) {
static_cast<UICodeEditor*>( client )->getDocumentRef() );
} );
doc.setCommand( "lsp-plugin-restart", [this] { mManager->reload( getId() ); } );
doc.setCommand( "lsp-plugin-restart", [lifetime = mLifetime.weakHandle()] {
lifetime.run(
[]( LSPClientPlugin* plugin ) { plugin->mManager->reload( plugin->getId() ); } );
} );
doc.setCommand( "lsp-show-document-symbols", [this]( TextDocument::Client* client ) {
showDocumentSymbols( static_cast<UICodeEditor*>( client ) );
@@ -1603,7 +1593,7 @@ void LSPClientPlugin::onUnregister( UICodeEditor* editor ) {
for ( auto& kb : mKeyBindings )
editor->getKeyBindings().removeCommandKeybind( kb.first );
if ( mShuttingDown )
if ( mShuttingDown && !mUnregistering )
return;
editor->removeActionsByTag( getMouseMoveHash( editor ) );
@@ -1628,8 +1618,8 @@ void LSPClientPlugin::onUnregister( UICodeEditor* editor ) {
}
if ( editor->hasDocument() )
for ( auto& kb : mKeyBindings )
editor->getDocument().removeCommand( kb.first );
for ( const auto* command : LSPDocumentCommands )
editor->getDocument().removeCommand( command );
{
Lock lds( mDocSymbolsMutex );
@@ -162,6 +162,8 @@ class LSPClientPlugin : public Plugin {
void load( PluginManager* pluginManager );
virtual void unregisterEditors();
void loadLSPConfig( std::vector<LSPDefinition>& lsps, const std::string& path,
bool updateConfigFile );
@@ -1289,11 +1289,6 @@ LSPClientServer::~LSPClientServer() {
mProcess.kill();
eeSAFE_DELETE( mSocket );
{
Lock l( mClientsMutex );
for ( const auto& client : mClients )
client.first->unregisterClient( client.second.get() );
}
}
bool LSPClientServer::socketConnect() {
@@ -1414,6 +1409,12 @@ bool LSPClientServer::registerDoc( const std::shared_ptr<TextDocument>& doc ) {
return true;
}
void LSPClientServer::detachDocuments() {
Lock l( mClientsMutex );
for ( const auto& client : mClients )
client.second->detach();
}
void LSPClientServer::notifyServerInitialized() {
for ( const auto& client : mClients )
client.second->onServerInitialized();
@@ -75,6 +75,8 @@ class LSPClientServer {
bool registerDoc( const std::shared_ptr<TextDocument>& doc );
void detachDocuments();
bool isRunning();
bool isReady() const;
@@ -145,6 +145,12 @@ void LSPClientServerManager::closeLSPServer( const String::HashType& id ) {
if ( mErasingClients.find( id ) != mErasingClients.end() )
return;
mErasingClients.insert( id );
{
Lock l( mClientsMutex );
auto it = mClients.find( id );
if ( it != mClients.end() )
it->second->detachDocuments();
}
mThreadPool->run( [this, id]() {
Lock l( mClientsMutex );
auto it = mClients.find( id );
@@ -158,6 +164,12 @@ void LSPClientServerManager::closeLSPServer( const String::HashType& id ) {
} );
}
void LSPClientServerManager::detachDocuments() {
Lock l( mClientsMutex );
for ( const auto& server : mClients )
server.second->detachDocuments();
}
void LSPClientServerManager::goToLocation( const LSPLocation& loc ) {
UICodeEditorSplitter* splitter = mPlugin->getManager()->getSplitter();
if ( nullptr == splitter )
@@ -25,6 +25,8 @@ class LSPClientServerManager {
// sync
void tryRunServer( const std::shared_ptr<TextDocument>& doc );
void detachDocuments();
size_t clientCount() const;
size_t lspCount() const;
@@ -40,17 +40,27 @@ void LSPDocumentClient::setupFoldRangeService() {
}
LSPDocumentClient::~LSPDocumentClient() {
mDoc->getFoldRangeService().setProvider( nullptr );
mDoc->unregisterClient( this );
detach();
while ( mRunningSemanticTokens || mProcessingSemanticTokensResponse )
Sys::sleep( Milliseconds( 0.1f ) );
mDoc = nullptr;
}
void LSPDocumentClient::detach() {
if ( mDetached )
return;
mDetached = true;
mShutdown = true;
if ( mDoc ) {
if ( mDoc->getFoldRangeService().getProvider() == this )
mDoc->getFoldRangeService().setProvider( nullptr );
mDoc->unregisterClient( this );
}
UISceneNode* sceneNode = getUISceneNode();
if ( nullptr != sceneNode && 0 != mTag )
sceneNode->removeActionsByTag( mTag );
if ( nullptr != sceneNode && 0 != mTagSemanticTokens )
sceneNode->removeActionsByTag( mTagSemanticTokens );
mShutdown = true;
while ( mRunningSemanticTokens || mProcessingSemanticTokensResponse )
Sys::sleep( Milliseconds( 0.1f ) );
}
bool LSPDocumentClient::tryRequestFoldRanges( bool requestFolds ) {
@@ -26,6 +26,8 @@ class LSPDocumentClient : public TextDocument::Client, public FoldRangeProvider
~LSPDocumentClient();
void detach();
virtual void onDocumentLoaded( TextDocument* );
virtual void onDocumentTextChanged( const DocumentContentChange& change );
virtual void onDocumentUndoRedo( const TextDocument::UndoRedo& eventType );
@@ -86,6 +88,7 @@ class LSPDocumentClient : public TextDocument::Client, public FoldRangeProvider
bool mWaitingSemanticTokensResponse{ false };
bool mProcessingSemanticTokensResponse{ false };
bool mShutdown{ false };
bool mDetached{ false };
bool mFirstHighlight{ true };
bool mAlreadyRequestedFoldingRanges{ false };
+17 -8
View File
@@ -27,6 +27,17 @@ bool Plugin::isShuttingDown() const {
return mShuttingDown;
}
void Plugin::shutdown() {
if ( mShuttingDown.exchange( true ) )
return;
waitUntilLoaded();
unsubscribeFileSystemListener();
mManager->unsubscribeMessages( this );
mUnregistering = true;
unregisterEditors();
mUnregistering = false;
}
bool Plugin::hasFileConfig() {
return !mConfigPath.empty();
}
@@ -249,13 +260,6 @@ void Plugin::createListView( UICodeEditor* editor, std::shared_ptr<Model> model,
PluginBase::~PluginBase() {
mShuttingDown = true;
unsubscribeFileSystemListener();
for ( auto editor : mEditors ) {
for ( auto listener : editor.second )
editor.first->removeEventListener( listener );
editor.first->unregisterPlugin( this );
}
}
void PluginBase::onRegister( UICodeEditor* editor ) {
@@ -306,7 +310,7 @@ void PluginBase::onRegister( UICodeEditor* editor ) {
void PluginBase::onUnregister( UICodeEditor* editor ) {
onBeforeUnregister( editor );
if ( mShuttingDown )
if ( mShuttingDown && !mUnregistering )
return;
Lock l( mMutex );
TextDocument* doc = mEditorDocs[editor];
@@ -342,4 +346,9 @@ void PluginBase::onUnregisterDocument( TextDocument* doc ) {
doc->removeCommand( kb.first );
}
void PluginBase::unregisterEditors() {
while ( !mEditors.empty() )
mEditors.begin()->first->unregisterPlugin( this );
}
} // namespace ecode
+128 -2
View File
@@ -28,6 +28,32 @@ class PluginManager;
class PluginContextProvider;
class SettingsPage;
/**
* Base class for ecode plugins.
*
* Plugin teardown has two distinct phases:
*
* 1. PluginManager calls shutdown() synchronously on the main thread. shutdown() waits for
* loading to finish, prevents new work, unsubscribes manager-owned callbacks, and calls
* unregisterEditors() while the complete derived object is still alive.
* 2. PluginManager destroys the plugin. Normal disable/reload may run the destructor on a worker
* thread; application shutdown may run it on the main thread.
*
* Implementers must detach everything that can call into the plugin from unregisterEditors():
* editor and document commands, keybindings, event listeners, document clients, application
* commands, UI widgets/actions, and queued main-thread callbacks. When unregisterEditors()
* returns, no application-owned object may retain a pointer or callback to the plugin.
*
* Destructors must only release plugin-owned resources. They may cancel or join workers and stop
* child processes or servers, but must not access editors, documents, UI objects, or PluginManager.
* Potentially blocking resource shutdown belongs in the destructor so normal unload does not
* stall the main thread.
*
* PluginManager owns the lifecycle: implementations must not call shutdown() themselves and must
* not defer application detachment to their destructor. PluginBase is the preferred base class
* when a plugin registers with editors or documents because it provides the common bookkeeping
* and cleanup hooks described below.
*/
class Plugin : public UICodeEditorPlugin {
public:
explicit Plugin( PluginManager* manager );
@@ -42,6 +68,15 @@ class Plugin : public UICodeEditorPlugin {
bool isShuttingDown() const;
/**
* Starts the main-thread application-detachment phase.
*
* This operation is idempotent and is called exclusively by PluginManager. It sets
* isShuttingDown(), waits for loading to complete, removes the plugin's filesystem and message
* subscriptions, and invokes unregisterEditors().
*/
void shutdown();
virtual bool hasFileConfig();
virtual std::string getFileConfigPath();
@@ -97,6 +132,7 @@ class Plugin : public UICodeEditorPlugin {
std::atomic<bool> mReady{ false };
std::atomic<bool> mLoading{ false };
std::atomic<bool> mShuttingDown{ false };
bool mUnregistering{ false };
void setReady( Time loadTime = Seconds( 0 ) );
@@ -104,14 +140,98 @@ class Plugin : public UICodeEditorPlugin {
bool editorExists( UICodeEditor* editor );
/**
* Detaches all application-owned references to this plugin.
*
* This is called by shutdown() on the main thread, with isShuttingDown() already true and while
* virtual dispatch still reaches the most-derived class. Implementations must be synchronous,
* must not wait for slow plugin-owned resources, and must leave no editor, document, UI,
* manager, or queued callback able to call the plugin.
*
* PluginBase subclasses overriding this method must call PluginBase::unregisterEditors()
* exactly once so its per-editor and per-document unregister hooks run.
*/
virtual void unregisterEditors() = 0;
static UIListView* createListViewHelper( UICodeEditor* editor, std::shared_ptr<Model> model,
const ModelEventCallback& onModelEventCb );
};
/**
* Plugin implementation helper for editor- and document-oriented plugins.
*
* PluginBase records every registered editor, document, and editor event listener. During
* shutdown, unregisterEditors() asks each editor to unregister the plugin. That invokes the
* following cleanup hooks while the derived plugin is still fully alive:
*
* - onBeforeUnregister(): removes keybindings declared in mKeyBindings;
* - onUnregisterEditor(): removes plugin-specific state owned by one editor;
* - onUnregisterDocument(): removes plugin commands and other per-document state after the last
* editor using that document is unregistered.
*
* The default onUnregisterDocument() removes commands named by mKeyBindings. A derived override
* that adds other commands must remove them and call PluginBase::onUnregisterDocument(). Event
* listener IDs added through onRegisterListeners() are removed automatically.
*
* Plugin implementer checklist:
*
* 1. Append every editor event listener ID to the vector passed to onRegisterListeners().
* 2. Undo per-editor registrations in onUnregisterEditor().
* 3. Undo per-document registrations in onUnregisterDocument(), then call the base implementation.
* 4. Override unregisterEditors() only for state not covered by those hooks: invalidate async
* callbacks, detach document clients, remove application commands, and close plugin UI. Call
* PluginBase::unregisterEditors() exactly once.
* 5. Keep only plugin-owned resource cleanup, such as joining workers or stopping servers, in the
* destructor.
*
* The unregister hooks are also called when individual editors and documents close, so they must
* work both during normal editor use and during full plugin shutdown.
*
* A typical implementation is:
*
* @code
* class ExamplePlugin : public PluginBase {
* public:
* ~ExamplePlugin() override {
* // No editor, document, UI, or PluginManager access here. This may block on a worker.
* mWorker.stopAndWait();
* }
*
* protected:
* void onRegisterDocument( TextDocument* doc ) override {
* doc->setCommand( "example-command", [this] { runCommand(); } );
* }
*
* void onRegisterListeners( UICodeEditor* editor,
* std::vector<Uint32>& listeners ) override {
* listeners.emplace_back(
* editor->on( Event::OnDocumentSave, [this]( const Event* ) { onSave(); } ) );
* }
*
* void onUnregisterDocument( TextDocument* doc ) override {
* doc->removeCommand( "example-command" );
* PluginBase::onUnregisterDocument( doc );
* }
*
* void unregisterEditors() override {
* // Cancel/invalidate callbacks first, then detach application-owned state.
* mLifetime.invalidate();
* removeApplicationCommand();
* PluginBase::unregisterEditors();
* closePluginUI();
* }
* };
* @endcode
*
* If no plugin-specific application state exists, do not override unregisterEditors(); the base
* implementation is sufficient. Never call onUnregisterEditor() or onUnregisterDocument()
* directly from the destructor.
*/
class PluginBase : public Plugin {
public:
explicit PluginBase( PluginManager* manager ) : Plugin( manager ) {}
/** Destructor finalizes plugin-owned state only; application detachment is already complete. */
virtual ~PluginBase();
virtual void onRegister( UICodeEditor* ) override;
@@ -144,17 +264,23 @@ class PluginBase : public Plugin {
virtual void onRegisterListeners( UICodeEditor*, std::vector<Uint32>& /*listeners*/ ) {};
//! Usually used to remove keybindings in an editor
//! Called before an editor is unregistered. The default removes mKeyBindings from the editor.
virtual void onBeforeUnregister( UICodeEditor* );
//! Registers commands and other state shared by all editors using the document.
virtual void onRegisterDocument( TextDocument* ) {}
//! Registers state owned by one editor. The default installs mKeyBindings.
virtual void onRegisterEditor( UICodeEditor* );
//! Removes plugin-specific state owned by one editor.
virtual void onUnregisterEditor( UICodeEditor* ) {}
//! Usually used to unregister commands in a document
//! Removes per-document state. The default removes commands named by mKeyBindings.
virtual void onUnregisterDocument( TextDocument* );
//! Unregisters every tracked editor and dispatches the cleanup hooks documented above.
virtual void unregisterEditors() override;
};
} // namespace ecode
+44 -8
View File
@@ -23,18 +23,27 @@ PluginManager::PluginManager( const std::string& resourcesPath, const std::strin
mLoadFileFn( loadFileCb ) {}
PluginManager::~PluginManager() {
mClosing = true;
beginShutdown();
for ( auto& plugin : mPlugins ) {
Log::debug( "PluginManager: unloading plugin %s", plugin.second->getTitle() );
eeDelete( plugin.second );
unloadPlugin( plugin.second );
}
unsubscribeFileSystemListener();
std::unique_lock<std::mutex> lock( mPendingUnloadsMutex );
mPendingUnloadsCondition.wait( lock, [this] { return mPendingUnloads == 0; } );
}
bool PluginManager::isClosing() const {
return mClosing;
}
void PluginManager::beginShutdown() {
if ( mClosing )
return;
mClosing = true;
mPluginReloadEnabled = false;
unsubscribeFileSystemListener();
}
void PluginManager::registerPlugin( const PluginDefinition& def ) {
mDefinitions[def.id] = def;
}
@@ -55,6 +64,8 @@ Plugin* ecode::PluginManager::get( const std::string& id ) {
}
bool PluginManager::setEnabled( const std::string& id, bool enable, bool sync ) {
if ( mClosing )
return false;
mPluginsEnabled[id] = enable;
Plugin* plugin = get( id );
if ( enable && plugin == nullptr && hasDefinition( id ) ) {
@@ -69,12 +80,8 @@ bool PluginManager::setEnabled( const std::string& id, bool enable, bool sync )
}
if ( !enable && plugin != nullptr ) {
Log::debug( "PluginManager: unloading plugin %s", mDefinitions[id].name );
mThreadPool->run( [plugin]() { eeDelete( plugin ); } );
{
Lock l( mSubscribedPluginsMutex );
mSubscribedPlugins.erase( id );
}
mPlugins.erase( id );
unloadPlugin( plugin );
}
return false;
}
@@ -84,6 +91,8 @@ bool PluginManager::isEnabled( const std::string& id ) const {
}
bool PluginManager::reload( const std::string& id ) {
if ( mClosing )
return false;
if ( !isPluginReloadEnabled() ) {
Log::warning( "PluginManager: tried to reload a plugin but plugin reload is not enabled." );
return false;
@@ -383,6 +392,33 @@ void PluginManager::registerFileSystemListener( Plugin* plugin ) {
mFileSystemListener->removeListener( listenerId );
}
void PluginManager::unloadPlugin( Plugin* plugin ) {
// Complete all application-facing teardown while virtual dispatch still reaches the complete
// plugin type. The destructor below may run on a worker and must only release owned resources.
unsubscribeFileSystemListener( plugin );
{
Lock l( mSubscribedPluginsMutex );
mSubscribedPlugins.erase( plugin->getId() );
}
plugin->shutdown();
if ( mClosing ) {
eeDelete( plugin );
return;
}
{
std::lock_guard<std::mutex> lock( mPendingUnloadsMutex );
++mPendingUnloads;
}
mThreadPool->run( [this, plugin] {
eeDelete( plugin );
{
std::lock_guard<std::mutex> lock( mPendingUnloadsMutex );
--mPendingUnloads;
}
mPendingUnloadsCondition.notify_one();
} );
}
void PluginManager::sendBroadcast( const PluginMessageType& notification,
const PluginMessageFormat& format, void* data ) {
if ( mClosing )
@@ -12,8 +12,10 @@
#include <eepp/ui/uiwindow.hpp>
#include <array>
#include <condition_variable>
#include <limits>
#include <memory>
#include <mutex>
#include <nlohmann/json.hpp>
#include <string>
@@ -365,6 +367,8 @@ class PluginManager {
bool isClosing() const;
void beginShutdown();
PluginContextProvider* getPluginContext() const { return mPluginContext; }
void forEachPlugin( std::function<void( Plugin* )> fn );
@@ -395,6 +399,9 @@ class PluginManager {
OnLoadFileCb mLoadFileFn;
UnorderedSet<Plugin*> mPluginsFSSubs;
UnorderedMap<Plugin*, Uint64> mPluginFSListenerIds;
std::mutex mPendingUnloadsMutex;
std::condition_variable mPendingUnloadsCondition;
size_t mPendingUnloads{ 0 };
bool mClosing{ false };
bool mPluginReloadEnabled{ false };
bool mPluginsDisabled{ false };
@@ -412,6 +419,8 @@ class PluginManager {
void unsubscribeFileSystemListener();
void registerFileSystemListener( Plugin* plugin );
void unloadPlugin( Plugin* plugin );
};
class PluginsModel : public Model {
@@ -230,6 +230,7 @@ void SpellCheckerPlugin::goToPrevError( UICodeEditor* editor ) {
void SpellCheckerPlugin::onUnregisterDocument( TextDocument* doc ) {
mDirtyDoc.erase( doc );
PluginBase::onUnregisterDocument( doc );
}
void SpellCheckerPlugin::onDocumentChanged( UICodeEditor*, TextDocument* oldDoc ) {
@@ -43,11 +43,6 @@ XMLToolsPlugin::XMLToolsPlugin( PluginManager* pluginManager, bool sync ) :
XMLToolsPlugin::~XMLToolsPlugin() {
waitUntilLoaded();
mShuttingDown = true;
{
Lock l( mClientsMutex );
for ( const auto& client : mClients )
client.first->unregisterClient( client.second.get() );
}
}
bool XMLToolsPlugin::getHighlightMatch() const {
@@ -122,8 +117,20 @@ void XMLToolsPlugin::onRegisterDocument( TextDocument* doc ) {
void XMLToolsPlugin::onUnregisterDocument( TextDocument* doc ) {
Lock l( mClientsMutex );
doc->unregisterClient( mClients[doc].get() );
mClients.erase( doc );
auto client = mClients.find( doc );
if ( client != mClients.end() ) {
doc->unregisterClient( client->second.get() );
mClients.erase( client );
}
PluginBase::onUnregisterDocument( doc );
}
void XMLToolsPlugin::unregisterEditors() {
PluginBase::unregisterEditors();
Lock l( mClientsMutex );
for ( const auto& client : mClients )
client.first->unregisterClient( client.second.get() );
mClients.clear();
}
bool XMLToolsPlugin::isOverMatch( TextDocument* doc, const Int64& index ) const {
@@ -131,6 +131,8 @@ class XMLToolsPlugin : public PluginBase {
virtual void onUnregisterDocument( TextDocument* doc ) override;
void unregisterEditors() override;
bool isOverMatch( TextDocument* doc, const Int64& index ) const;
bool isVisibleInRange( TextDocument* doc, const DocumentLineRange& docLineRange );