Optimize editor initialization and multi-file diff loading

- Share immutable built-in document commands, editor command tables, default keybindings, and syntax color scheme storage across editor instances. Preserve mutations through copy-on-write storage and explicit command-removal tracking.
  - Use ShortcutMap consistently with unordered lookup while retaining deterministic ordering where serialization or conflict resolution depends on it. Cache ambiguous syntax-extension checks and invalidate the cache when language definitions change.
  - Prepare Git commit-detail diffs and sub-line changes on worker threads with cancellation and stale-request protection. Release raw patch memory before UI construction and add regression coverage for prepared and cancelled diffs.
  - Add Diff Changed support and enable collapsible file headers for Diff HEAD, Diff Staged, and Diff Changes.
  - Update translations, compatibility call sites, tests, and the efsw dependency.
This commit is contained in:
Martín Lucas Golini
2026-08-30 17:35:50 -03:00
parent 3afcb3f2d2
commit 8e5d180a4d
45 changed files with 937 additions and 489 deletions
+25 -21
View File
@@ -1231,15 +1231,15 @@ UITabWidget* App::getSidePanel() const {
return mSidePanel;
}
const std::map<KeyBindings::Shortcut, std::string>& App::getRealLocalKeybindings() const {
const KeyBindings::ShortcutMap& App::getRealLocalKeybindings() const {
return mRealLocalKeybindings;
}
const std::map<KeyBindings::Shortcut, std::string>& App::getRealSplitterKeybindings() const {
const KeyBindings::ShortcutMap& App::getRealSplitterKeybindings() const {
return mRealSplitterKeybindings;
}
const std::map<KeyBindings::Shortcut, std::string>& App::getRealTerminalKeybindings() const {
const KeyBindings::ShortcutMap& App::getRealTerminalKeybindings() const {
return mRealTerminalKeybindings;
}
@@ -1510,32 +1510,35 @@ void App::loadKeybindings() {
getMigrateKeybindings(), mConfig.iniState );
auto localKeybindings = getLocalKeybindings();
for ( const auto& kb : localKeybindings ) {
auto found = mKeybindingsInvert.find( kb.second );
for ( const auto& shortcut : KeyBindings::getOrderedShortcuts( localKeybindings ) ) {
const auto& command = localKeybindings.find( shortcut )->second;
auto found = mKeybindingsInvert.find( command );
if ( found != mKeybindingsInvert.end() ) {
mRealLocalKeybindings[bindings.getShortcutFromString( found->second )] = kb.second;
mRealLocalKeybindings[bindings.getShortcutFromString( found->second )] = command;
} else {
mRealLocalKeybindings[kb.first] = kb.second;
mRealLocalKeybindings[shortcut] = command;
}
}
auto localSplitterKeybindings = UICodeEditorSplitter::getLocalDefaultKeybindings();
for ( const auto& kb : localSplitterKeybindings ) {
auto found = mKeybindingsInvert.find( kb.second );
for ( const auto& shortcut : KeyBindings::getOrderedShortcuts( localSplitterKeybindings ) ) {
const auto& command = localSplitterKeybindings.find( shortcut )->second;
auto found = mKeybindingsInvert.find( command );
if ( found != mKeybindingsInvert.end() ) {
mRealSplitterKeybindings[bindings.getShortcutFromString( found->second )] = kb.second;
mRealSplitterKeybindings[bindings.getShortcutFromString( found->second )] = command;
} else {
mRealSplitterKeybindings[kb.first] = kb.second;
mRealSplitterKeybindings[shortcut] = command;
}
}
auto localTerminalKeybindings = TerminalManager::getTerminalKeybindings();
for ( const auto& kb : localTerminalKeybindings ) {
auto found = mKeybindingsInvert.find( kb.second );
for ( const auto& shortcut : KeyBindings::getOrderedShortcuts( localTerminalKeybindings ) ) {
const auto& command = localTerminalKeybindings.find( shortcut )->second;
auto found = mKeybindingsInvert.find( command );
if ( found != mKeybindingsInvert.end() ) {
mRealTerminalKeybindings[bindings.getShortcutFromString( found->second )] = kb.second;
mRealTerminalKeybindings[bindings.getShortcutFromString( found->second )] = command;
} else {
mRealTerminalKeybindings[kb.first] = kb.second;
mRealTerminalKeybindings[shortcut] = command;
}
}
}
@@ -2100,7 +2103,7 @@ const AppConfig& App::getConfig() const {
return mConfig;
}
const std::map<KeyBindings::Shortcut, std::string>& App::getRealDefaultKeybindings() {
const KeyBindings::ShortcutMap& App::getRealDefaultKeybindings() {
if ( mRealDefaultKeybindings.empty() ) {
mRealDefaultKeybindings.insert( mRealLocalKeybindings.begin(),
mRealLocalKeybindings.end() );
@@ -2112,7 +2115,7 @@ const std::map<KeyBindings::Shortcut, std::string>& App::getRealDefaultKeybindin
return mRealDefaultKeybindings;
}
std::map<KeyBindings::Shortcut, std::string> App::getDefaultKeybindings() {
KeyBindings::ShortcutMap App::getDefaultKeybindings() {
auto bindings = UICodeEditorSplitter::getDefaultKeybindings();
auto local = getLocalKeybindings();
auto app = TerminalManager::getTerminalKeybindings();
@@ -2127,7 +2130,7 @@ static Uint32 DefaultSwitchToStatusPanelModifier = KeyMod::getDefaultModifier();
static Uint32 DefaultSwitchToStatusPanelModifier = KeyMod::getDefaultSecondaryModifier();
#endif
std::map<KeyBindings::Shortcut, std::string> App::getLocalKeybindings() {
KeyBindings::ShortcutMap App::getLocalKeybindings() {
return {
{ { KEY_PRINTSCREEN, KEYMOD_NONE }, "take-screenshot" },
{ { KEY_RETURN, KeyMod::getDefaultSecondaryModifier() | KeyMod::getDefaultModifier() },
@@ -2626,15 +2629,16 @@ void App::loadAudioFromPath( const std::string& path, bool autoPlay ) {
}
void App::loadDiffFromMemory( const std::string& content, const std::string& originalFilePath,
const std::string& oldFilePath, const std::string& repoPath ) {
const std::string& oldFilePath, const std::string& repoPath,
bool interactiveFileHeaders ) {
if ( UIDiffView::isMultiFileDiff( content ) ) {
auto diffViewTitle = i18n( "diff_viewer", "Diff Viewer" ) + ": " + originalFilePath;
UIIcon* icon = getUISceneNode()->findIcon( "filetype-diff" );
if ( !icon )
icon = getUISceneNode()->findIcon( "file" );
auto scrollView =
UIDiffView::NewMultiFileDiffViewer( content, repoPath, mConfig.editor.diffViewMode );
auto scrollView = UIDiffView::NewMultiFileDiffViewer(
content, repoPath, mConfig.editor.diffViewMode, interactiveFileHeaders );
auto [tab, iv] = getSplitter()->createWidget( scrollView, diffViewTitle );
if ( icon )
tab->setIcon( icon->createDrawable( getMenuIconSize() ) );
+12 -12
View File
@@ -179,11 +179,11 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
DrawablePtr findIcon( const std::string& name, const size_t iconSize );
const std::map<KeyBindings::Shortcut, std::string>& getRealDefaultKeybindings();
const KeyBindings::ShortcutMap& getRealDefaultKeybindings();
std::map<KeyBindings::Shortcut, std::string> getDefaultKeybindings();
KeyBindings::ShortcutMap getDefaultKeybindings();
std::map<KeyBindings::Shortcut, std::string> getLocalKeybindings();
KeyBindings::ShortcutMap getLocalKeybindings();
std::map<std::string, std::string> getMigrateKeybindings();
@@ -581,8 +581,8 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
void loadDiffFromPaths( const std::string& oldPath, const std::string& newPath );
void loadDiffFromMemory( const std::string& content, const std::string& originalFilePath = "",
const std::string& oldFilePath = "",
const std::string& repoPath = "" );
const std::string& oldFilePath = "", const std::string& repoPath = "",
bool interactiveFileHeaders = false );
void loadDiffFromStrings( const std::string& str, const std::string& otherStr );
void configureDiffView( UIDiffView* diffView );
@@ -619,11 +619,11 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
UITabWidget* getSidePanel() const;
const std::map<KeyBindings::Shortcut, std::string>& getRealLocalKeybindings() const;
const KeyBindings::ShortcutMap& getRealLocalKeybindings() const;
const std::map<KeyBindings::Shortcut, std::string>& getRealSplitterKeybindings() const;
const KeyBindings::ShortcutMap& getRealSplitterKeybindings() const;
const std::map<KeyBindings::Shortcut, std::string>& getRealTerminalKeybindings() const;
const KeyBindings::ShortcutMap& getRealTerminalKeybindings() const;
const std::string& getFileToOpen() const;
@@ -699,10 +699,10 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
std::unordered_map<std::string, std::string> mGlobalSearchKeybindings;
std::unordered_map<std::string, std::string> mDocumentSearchKeybindings;
std::unordered_map<std::string, std::string> mStatusBarKeybindings;
std::map<KeyBindings::Shortcut, std::string> mRealLocalKeybindings;
std::map<KeyBindings::Shortcut, std::string> mRealSplitterKeybindings;
std::map<KeyBindings::Shortcut, std::string> mRealTerminalKeybindings;
std::map<KeyBindings::Shortcut, std::string> mRealDefaultKeybindings;
KeyBindings::ShortcutMap mRealLocalKeybindings;
KeyBindings::ShortcutMap mRealSplitterKeybindings;
KeyBindings::ShortcutMap mRealTerminalKeybindings;
KeyBindings::ShortcutMap mRealDefaultKeybindings;
std::unordered_map<std::string, std::string> mMousebindings;
std::unordered_map<std::string, std::string> mMousebindingsInvert;
std::string mConfigPath;
+15 -12
View File
@@ -72,17 +72,19 @@ void KeybindingsHelper::updateKeybindings(
IniFile& ini, const std::string& group, Input* input,
std::unordered_map<std::string, std::string>& keybindings,
std::unordered_map<std::string, std::string>& invertedKeybindings,
const std::map<KeyBindings::Shortcut, std::string>& defKeybindings, bool forceRebind,
const KeyBindings::ShortcutMap& defKeybindings, bool forceRebind,
const std::map<std::string, std::string>& migrateKeyindings, IniFile& iniState ) {
KeyBindings bindings( input );
bool added = false;
bool migrated = false;
const auto orderedDefaultShortcuts = KeyBindings::getOrderedShortcuts( defKeybindings );
if ( ini.findKey( group ) != IniFile::noID ) {
keybindings = ini.getKeyUnorderedMap( group );
} else {
for ( const auto& it : defKeybindings )
ini.setValue( group, bindings.getShortcutString( it.first ), it.second );
for ( const auto& shortcut : orderedDefaultShortcuts )
ini.setValue( group, bindings.getShortcutString( shortcut ),
defKeybindings.find( shortcut )->second );
added = true;
}
for ( const auto& key : keybindings )
@@ -93,9 +95,9 @@ void KeybindingsHelper::updateKeybindings(
auto foundCmd = invertedKeybindings.find( migrate.first );
if ( foundCmd != invertedKeybindings.end() && foundCmd->second == migrate.second ) {
KeyBindings::Shortcut shortcut;
for ( const auto& defKb : defKeybindings ) {
if ( defKb.second == foundCmd->first ) {
shortcut = defKb.first;
for ( const auto& defaultShortcut : orderedDefaultShortcuts ) {
if ( defKeybindings.find( defaultShortcut )->second == foundCmd->first ) {
shortcut = defaultShortcut;
break;
}
}
@@ -121,15 +123,16 @@ void KeybindingsHelper::updateKeybindings(
bool keybindingsWereEmpty = keybindings.empty();
if ( defKeybindings.size() != keybindings.size() || forceRebind ) {
for ( auto& key : defKeybindings ) {
auto foundCmd = invertedKeybindings.find( key.second );
auto shortcutStr = bindings.getShortcutString( key.first );
for ( const auto& shortcut : orderedDefaultShortcuts ) {
const auto& command = defKeybindings.find( shortcut )->second;
auto foundCmd = invertedKeybindings.find( command );
auto shortcutStr = bindings.getShortcutString( shortcut );
if ( ( foundCmd == invertedKeybindings.end() || keybindingsWereEmpty ) &&
keybindings.find( shortcutStr ) == keybindings.end() ) {
keybindings[shortcutStr] = key.second;
invertedKeybindings[key.second] = shortcutStr;
ini.setValue( group, shortcutStr, key.second );
keybindings[shortcutStr] = command;
invertedKeybindings[command] = shortcutStr;
ini.setValue( group, shortcutStr, command );
added = true;
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ class KeybindingsHelper {
IniFile& ini, const std::string& group, Input* input,
std::unordered_map<std::string, std::string>& keybindings,
std::unordered_map<std::string, std::string>& invertedKeybindings,
const std::map<KeyBindings::Shortcut, std::string>& defKeybindings, bool forceRebind,
const KeyBindings::ShortcutMap& defKeybindings, bool forceRebind,
const std::map<std::string, std::string>& migrateKeyindings, IniFile& iniState );
static void
@@ -124,7 +124,7 @@ UIWidget* UIBreakpointsTableView::createCell( UIWidget* rowWidget, const ModelIn
return UITableView::createCell( rowWidget, index );
}
const std::map<KeyBindings::Shortcut, std::string>
const KeyBindings::ShortcutMap
StatusDebuggerController::getLocalDefaultKeybindings() {
return {
{ { KEY_TAB, UICodeEditorSplitter::getDefaultSwitchToTabModifier() }, "next-tab" },
@@ -42,7 +42,7 @@ class StatusDebuggerController : public StatusBarElement, public UITabWidgetSpli
public:
enum class State { NotStarted, Running, Paused };
static const std::map<KeyBindings::Shortcut, std::string> getLocalDefaultKeybindings();
static const KeyBindings::ShortcutMap getLocalDefaultKeybindings();
StatusDebuggerController( UISplitter* mainSplitter, UISceneNode* uiSceneNode,
PluginContextProvider* pluginContext );
+2
View File
@@ -877,6 +877,8 @@ Git::Result Git::diff( DiffMode mode, const std::string& projectDir ) {
modeTxt = "--staged";
break;
}
case DiffChanged:
break;
}
return gitSimple( String::format( "diff %s", modeTxt ), projectDir );
}
+1 -1
View File
@@ -306,7 +306,7 @@ class Git {
bool isEmpty() const { return name.empty(); }
};
enum DiffMode { DiffHead, DiffStaged };
enum DiffMode { DiffHead, DiffStaged, DiffChanged };
Git( const std::string& projectDir = "", const std::string& gitPath = "" );
+35 -7
View File
@@ -212,6 +212,8 @@ GitPlugin::~GitPlugin() {
mLifetime.invalidate();
waitUntilLoaded();
mShuttingDown = true;
mCommitDetails.cancelDiffPreparation();
mDetachedHistory.details.cancelDiffPreparation();
mConflictViewCloseConnection.disconnect();
mConflictView = nullptr;
mConflictSessions.clear();
@@ -2152,10 +2154,13 @@ void GitPlugin::diff( const Git::DiffMode mode, const std::string& repoPath ) {
case Git::DiffStaged:
modeName = "staged";
break;
case Git::DiffChanged:
modeName = "changed";
break;
}
plugin->getPluginContext()->loadDiffFromMemory(
res.result, UIDiffView::isMultiFileDiff( res.result ) ? modeName : "", "",
repoPath );
res.result, UIDiffView::isMultiFileDiff( res.result ) ? modeName : "", "", repoPath,
true );
} );
} );
}
@@ -2992,6 +2997,8 @@ void GitPlugin::CommitDetailsState::openCommitDetails( GitPlugin& plugin, const
if ( commit.hash.empty() && !isWorkingTree )
return;
const std::string selectedRepo = plugin.repoSelected();
cancelDiffPreparation();
diffPreparationCancelled = std::make_shared<std::atomic_bool>( false );
const Uint64 requestGeneration = ++generation;
this->commit = commit;
workingTree = isWorkingTree;
@@ -3163,14 +3170,31 @@ void GitPlugin::CommitDetailsState::loadCommitFiles( GitPlugin& plugin, bool det
const std::string repo = this->repo;
const Git::Commit commit = this->commit;
const bool workingTree = this->workingTree;
const auto diffPreparationCancelled = this->diffPreparationCancelled;
auto git = plugin.mGit;
const auto lifetime = plugin.mLifetime.weakHandle();
plugin.runAsyncTask( [git = std::move( git ), lifetime, generation, repo, commit, detached,
workingTree] {
workingTree, diffPreparationCancelled] {
if ( diffPreparationCancelled &&
diffPreparationCancelled->load( std::memory_order_relaxed ) )
return;
auto result =
workingTree ? git->workingTreeFiles( repo ) : git->commitFiles( commit, repo );
if ( diffPreparationCancelled &&
diffPreparationCancelled->load( std::memory_order_relaxed ) )
return;
std::shared_ptr<UIDiffView::PreparedMultiFileDiff> preparedDiff;
if ( result.success() && !result.patch.empty() ) {
preparedDiff =
UIDiffView::prepareMultiFileDiff( result.patch, diffPreparationCancelled );
if ( diffPreparationCancelled &&
diffPreparationCancelled->load( std::memory_order_relaxed ) )
return;
std::string{}.swap( result.patch );
}
lifetime.run( [generation, repo, commit, detached, workingTree,
result = std::move( result )]( GitPlugin* plugin ) mutable {
result = std::move( result ),
preparedDiff = std::move( preparedDiff )]( GitPlugin* plugin ) mutable {
auto& details = detached ? plugin->mDetachedHistory.details : plugin->mCommitDetails;
if ( plugin->mShuttingDown || generation != details.generation ||
repo != details.repo || repo != plugin->repoSelected() ||
@@ -3230,9 +3254,9 @@ void GitPlugin::CommitDetailsState::loadCommitFiles( GitPlugin& plugin, bool det
details.gitHub->setVisible( !details.url.empty() );
details.diffContainer->closeAllChildren();
details.diff = nullptr;
if ( !result.patch.empty() ) {
details.diff =
UIDiffView::NewMultiFileDiffViewer( result.patch, repo, details.viewMode );
if ( preparedDiff ) {
details.diff = UIDiffView::NewMultiFileDiffViewer( std::move( preparedDiff ), repo,
details.viewMode );
details.diff->setLayoutSizePolicy( SizePolicy::MatchParent,
SizePolicy::MatchParent );
details.diff->setParent( details.diffContainer );
@@ -3750,6 +3774,8 @@ void GitPlugin::buildSidePanelTab() {
"diff-added" );
if ( type == Git::GitStatusType::Changed ) {
menuAdd( menu, "git-diff-changed",
i18n( "git_diff_changed", "Diff Changed" ), "diff-multiple" );
menu->addSeparator();
menuAdd( menu, "git-discard-all",
i18n( "git_discard_all", "Discard All" ) );
@@ -3775,6 +3801,8 @@ void GitPlugin::buildSidePanelTab() {
(Uint32)Git::GitStatusType::Changed ) );
} else if ( id == "git-diff-staged" ) {
diff( Git::DiffMode::DiffStaged, repoPath );
} else if ( id == "git-diff-changed" ) {
diff( Git::DiffMode::DiffChanged, repoPath );
}
} );
@@ -188,6 +188,7 @@ class GitPlugin : public PluginBase {
Git::Commit commit;
std::string repo;
std::atomic<Uint64> generation{ 0 };
std::shared_ptr<std::atomic_bool> diffPreparationCancelled;
EventConnection closeConnection;
Tools::UIDiffView::ViewMode viewMode{ Tools::UIDiffView::ViewMode::Unified };
bool messageExpanded{ false };
@@ -199,7 +200,14 @@ class GitPlugin : public PluginBase {
void loadCommitFiles( GitPlugin& plugin, bool detached );
void cancelDiffPreparation() {
if ( diffPreparationCancelled )
diffPreparationCancelled->store( true, std::memory_order_relaxed );
diffPreparationCancelled.reset();
}
void reset() {
cancelDiffPreparation();
++generation;
view = nullptr;
subject = nullptr;
@@ -1,6 +1,7 @@
#pragma once
#include <eepp/core/string.hpp>
#include <eepp/graphics/drawable.hpp>
#include <eepp/ui/doc/textrange.hpp>
#include <optional>
#include <string>
@@ -170,7 +171,8 @@ class PluginContextProvider {
virtual void loadDiffFromMemory( const std::string& content,
const std::string& originalFilePath = "",
const std::string& oldFilePath = "",
const std::string& repoPath = "" ) = 0;
const std::string& repoPath = "",
bool interactiveFileHeaders = false ) = 0;
virtual void loadFolder( std::string path, bool forceNewWindow = false ) = 0;
+1 -1
View File
@@ -131,7 +131,7 @@ void TerminalManager::loadTerminalColorSchemes() {
mTerminalCurrentColorScheme = mTerminalColorSchemes.begin()->first;
}
std::map<KeyBindings::Shortcut, std::string> TerminalManager::getTerminalKeybindings() {
KeyBindings::ShortcutMap TerminalManager::getTerminalKeybindings() {
return {
{ { KEY_T, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "create-new-terminal" },
{ { KEY_E,
+1 -1
View File
@@ -34,7 +34,7 @@ class TerminalManager {
void loadTerminalColorSchemes();
static std::map<KeyBindings::Shortcut, std::string> getTerminalKeybindings();
static KeyBindings::ShortcutMap getTerminalKeybindings();
const std::string& getTerminalColorSchemesPath() const;
+1 -1
View File
@@ -10,7 +10,7 @@
namespace ecode {
static const std::map<KeyBindings::Shortcut, std::string> getDefaultKeybindings() {
static const KeyBindings::ShortcutMap getDefaultKeybindings() {
return {
{ { KEY_C, KeyMod::getDefaultModifier() }, "copy" },
{ { KEY_X, KeyMod::getDefaultModifier() }, "cut" },