From 8ce9e7045bd95556a106fd6e40992dd757bcf7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 14 Jul 2024 20:15:04 -0300 Subject: [PATCH] Auto-Save implementation for project files (issue SpartanJ/ecode#220). Still pending Auto-Save for non-open project environment. --- include/eepp/system/inifile.hpp | 9 +- include/eepp/ui/doc/textdocument.hpp | 12 +- include/eepp/ui/doc/textundostack.hpp | 70 +---------- src/eepp/system/inifile.cpp | 30 ++--- src/eepp/ui/doc/textdocument.cpp | 40 ++++-- src/eepp/ui/doc/textundostack.cpp | 172 ++++++++++++++++++++++++++ src/tools/ecode/appconfig.cpp | 170 ++++++++++++++++++++++--- src/tools/ecode/appconfig.hpp | 18 ++- src/tools/ecode/ecode.cpp | 27 ++-- src/tools/ecode/ecode.hpp | 6 +- src/tools/ecode/settingsmenu.cpp | 12 ++ 11 files changed, 439 insertions(+), 127 deletions(-) diff --git a/include/eepp/system/inifile.hpp b/include/eepp/system/inifile.hpp index ff61585e0..5a35ed7f7 100644 --- a/include/eepp/system/inifile.hpp +++ b/include/eepp/system/inifile.hpp @@ -23,10 +23,6 @@ #include #include -#define MAX_KEYNAME 128 -#define MAX_VALUENAME 128 -#define MAX_VALUEDATA 2048 - namespace EE { namespace System { class Pack; @@ -65,7 +61,7 @@ class EE_API IniFile { void path( const std::string& newPath ) { mPath = newPath; } /** @return The ini file path */ - std::string path() const { return mPath; } + const std::string& path() const { return mPath; } /** Reads ini file specified using mPath. * @return true if successful, false otherwise. */ @@ -74,6 +70,9 @@ class EE_API IniFile { /** Writes data stored in class to ini file. */ bool writeFile(); + /** Writes data stored in class to a IOStream. */ + bool writeStream( IOStream& stream ); + /** Deletes all stored ini data. */ void clear(); diff --git a/include/eepp/ui/doc/textdocument.hpp b/include/eepp/ui/doc/textdocument.hpp index 1d99b2c1b..630352925 100644 --- a/include/eepp/ui/doc/textdocument.hpp +++ b/include/eepp/ui/doc/textdocument.hpp @@ -631,6 +631,14 @@ class EE_API TextDocument { void setLines( std::vector&& lines ); + std::string serializeUndoRedo( bool inverted ); + + void unserializeUndoRedo( const std::string& jsonString ); + + void changeFilePath( const std::string& filePath ); + + void setDirtyUntilSave(); + protected: friend class TextUndoStack; friend class FoldRangeServive; @@ -672,7 +680,7 @@ class EE_API TextDocument { Clock mTimer; SyntaxDefinition mSyntaxDefinition; std::string mDefaultFileName; - Uint64 mCleanChangeId; + Uint64 mCleanChangeId{ 0 }; Uint32 mPageSize{ 10 }; UnorderedMap mCommands; UnorderedMap mRefCommands; @@ -754,6 +762,8 @@ class EE_API TextDocument { bool wholeWord = false, FindReplaceType type = FindReplaceType::Normal, TextRange restrictRange = TextRange() ); + + void changeFilePath( const std::string& filePath, bool notify ); }; struct TextSearchParams { diff --git a/include/eepp/ui/doc/textundostack.hpp b/include/eepp/ui/doc/textundostack.hpp index 60b4b4310..67b778d90 100644 --- a/include/eepp/ui/doc/textundostack.hpp +++ b/include/eepp/ui/doc/textundostack.hpp @@ -12,71 +12,9 @@ using namespace EE::System; namespace EE { namespace UI { namespace Doc { class TextDocument; +class TextUndoCommand; -enum class TextUndoCommandType { Insert, Remove, Selection }; - -class EE_API TextUndoCommand { - public: - TextUndoCommand( const Uint64& id, const TextUndoCommandType& type, const Time& timestamp ); - - virtual ~TextUndoCommand(); - - const Uint64& getId() const; - - const TextUndoCommandType& getType() const; - - const Time& getTimestamp() const; - - protected: - Uint64 mId; - TextUndoCommandType mType; - Time mTimestamp; -}; - -class EE_API TextUndoCommandInsert : public TextUndoCommand { - public: - TextUndoCommandInsert( const Uint64& id, const size_t& cursorIdx, const String& text, - const TextPosition& position, const Time& timestamp ); - - const String& getText() const; - - const TextPosition& getPosition() const; - - size_t getCursorIdx() const; - - protected: - String mText; - TextPosition mPosition; - size_t mCursorIdx; -}; - -class EE_API TextUndoCommandRemove : public TextUndoCommand { - public: - TextUndoCommandRemove( const Uint64& id, const size_t& cursorIdx, const TextRange& range, - const Time& timestamp ); - - const TextRange& getRange() const; - - size_t getCursorIdx() const; - - protected: - TextRange mRange; - size_t mCursorIdx; -}; - -class EE_API TextUndoCommandSelection : public TextUndoCommand { - public: - TextUndoCommandSelection( const Uint64& id, const size_t& cursorIdx, - const TextRanges& selection, const Time& timestamp ); - - const TextRanges& getSelection() const; - - size_t getCursorIdx() const; - - protected: - TextRanges mSelection; - size_t mCursorIdx; -}; +enum class TextUndoCommandType { Insert = 1, Remove = 2, Selection = 3 }; using UndoStackContainer = std::deque; @@ -108,6 +46,10 @@ class EE_API TextUndoStack { Uint64 getCurrentChangeId() const; + std::string toJSON( bool inverted ); + + void fromJSON( const std::string& json ); + protected: friend class TextDocument; diff --git a/src/eepp/system/inifile.cpp b/src/eepp/system/inifile.cpp index 0cfc00468..fd9522318 100644 --- a/src/eepp/system/inifile.cpp +++ b/src/eepp/system/inifile.cpp @@ -8,6 +8,10 @@ #include #include +#define MAX_KEYNAME 128 +#define MAX_VALUENAME 128 +#define MAX_VALUEDATA 2048 + namespace EE { namespace System { IniFile::IniFile( const std::string& iniPath, bool autoLoad ) { @@ -159,51 +163,47 @@ bool IniFile::readFile() { } bool IniFile::writeFile() { - unsigned commentID, keyID, valueID; - IOStreamFile f( mPath, "w" ); + return writeStream( f ); +} - if ( !f.isOpen() ) +bool IniFile::writeStream( IOStream& stream ) { + if ( !stream.isOpen() ) return false; + unsigned commentID, keyID, valueID; std::string str; // Write header mComments. for ( commentID = 0; commentID < mComments.size(); ++commentID ) { str = ';' + mComments[commentID] + '\n'; - - f.write( str.c_str(), str.size() ); + stream.write( str.c_str(), str.size() ); } if ( mComments.size() ) { str = "\n"; - - f.write( str.c_str(), str.size() ); + stream.write( str.c_str(), str.size() ); } // Write Keys and values. for ( keyID = 0; keyID < mKeys.size(); ++keyID ) { str = '[' + mNames[keyID] + ']' + '\n'; - - f.write( str.c_str(), str.size() ); + stream.write( str.c_str(), str.size() ); // Comments. for ( commentID = 0; commentID < mKeys[keyID].comments.size(); ++commentID ) { str = ';' + mKeys[keyID].comments[commentID] + '\n'; - - f.write( str.c_str(), str.size() ); + stream.write( str.c_str(), str.size() ); } // Values. for ( valueID = 0; valueID < mKeys[keyID].names.size(); ++valueID ) { str = mKeys[keyID].names[valueID] + '=' + mKeys[keyID].values[valueID] + '\n'; - - f.write( str.c_str(), str.size() ); + stream.write( str.c_str(), str.size() ); } str = "\n"; - - f.write( str.c_str(), str.size() ); + stream.write( str.c_str(), str.size() ); } return true; diff --git a/src/eepp/ui/doc/textdocument.cpp b/src/eepp/ui/doc/textdocument.cpp index ce46575a3..da061f699 100644 --- a/src/eepp/ui/doc/textdocument.cpp +++ b/src/eepp/ui/doc/textdocument.cpp @@ -458,10 +458,7 @@ bool TextDocument::isBOM() const { } void TextDocument::notifyDocumentMoved( const std::string& path ) { - mFilePath = path; - mFileURI = URI( "file://" + mFilePath ); - mFileRealPath = FileInfo::isLink( mFilePath ) ? FileInfo( FileInfo( mFilePath ).linksTo() ) - : FileInfo( mFilePath ); + changeFilePath( path, false ); notifyDocumentMoved(); } @@ -503,19 +500,14 @@ TextDocument::LoadStatus TextDocument::loadFromFile( const std::string& path ) { std::string pathFix( path ); Pack* pack = PackManager::instance()->exists( pathFix ); if ( NULL != pack ) { - mFilePath = pathFix; - mFileRealPath = FileInfo(); - mFileURI = URI( "file://" + mFilePath ); + changeFilePath( pathFix, false ); return loadFromPack( pack, pathFix ); } } IOStreamFile file( path, "rb" ); auto ret = loadFromStream( file, path, true ); - mFilePath = path; - mFileURI = URI( "file://" + mFilePath ); - mFileRealPath = FileInfo::isLink( mFilePath ) ? FileInfo( FileInfo( mFilePath ).linksTo() ) - : FileInfo( mFilePath ); + changeFilePath( path, false ); resetSyntax(); mLoading = false; if ( !mLoadingAsync ) @@ -3004,6 +2996,32 @@ void TextDocument::setLines( std::vector&& lines ) { mLines = std::move( lines ); } +std::string TextDocument::serializeUndoRedo( bool inverted ) { + return mUndoStack.toJSON( inverted ); +} + +void TextDocument::unserializeUndoRedo( const std::string& jsonString ) { + return mUndoStack.fromJSON( jsonString ); +} + +void TextDocument::changeFilePath( const std::string& filePath ) { + changeFilePath( filePath, true ); +} + +void TextDocument::setDirtyUntilSave() { + mCleanChangeId = std::numeric_limits::max(); + notifySelectionChanged(); +} + +void TextDocument::changeFilePath( const std::string& filePath, bool notify ) { + mFilePath = filePath; + mFileURI = URI( "file://" + mFilePath ); + mFileRealPath = FileInfo::isLink( mFilePath ) ? FileInfo( FileInfo( mFilePath ).linksTo() ) + : FileInfo( mFilePath ); + if ( notify ) + notifyDocumentMoved(); +} + static inline void changeDepth( SyntaxHighlighter* highlighter, int& depth, const TextPosition& pos, int dir ) { if ( highlighter ) { diff --git a/src/eepp/ui/doc/textundostack.cpp b/src/eepp/ui/doc/textundostack.cpp index 4602b1476..6c0b20545 100644 --- a/src/eepp/ui/doc/textundostack.cpp +++ b/src/eepp/ui/doc/textundostack.cpp @@ -1,11 +1,132 @@ #include +#include #include #include +#include + using namespace EE::System; namespace EE { namespace UI { namespace Doc { +using json = nlohmann::json; + +class TextUndoCommand { + public: + TextUndoCommand( const Uint64& id, const TextUndoCommandType& type, const Time& timestamp ); + + virtual ~TextUndoCommand(); + + const Uint64& getId() const; + + const TextUndoCommandType& getType() const; + + const Time& getTimestamp() const; + + virtual json toJSON() = 0; + + protected: + Uint64 mId; + TextUndoCommandType mType; + Time mTimestamp; + + json baseJSON() { + json j; + j["type"] = mType; + j["timestamp"] = mTimestamp.toString(); + return j; + } +}; + +class TextUndoCommandInsert : public TextUndoCommand { + public: + TextUndoCommandInsert( const Uint64& id, const size_t& cursorIdx, const String& text, + const TextPosition& position, const Time& timestamp ); + + const String& getText() const; + + const TextPosition& getPosition() const; + + size_t getCursorIdx() const; + + json toJSON() { + auto j = baseJSON(); + j["text"] = mText.toUtf8(); + j["position"] = mPosition.toString(); + j["cursorIdx"] = mCursorIdx; + return j; + } + + static TextUndoCommandInsert* fromJSON( json j, Uint64 id ) { + auto timestamp = Time::fromString( j["timestamp"].get() ); + auto text = String::fromUtf8( j["text"].get() ); + auto position = TextPosition::fromString( j["position"].get() ); + auto cursorIdx = j["cursorIdx"].get(); + return eeNew( TextUndoCommandInsert, ( id, cursorIdx, text, position, timestamp ) ); + } + + protected: + String mText; + TextPosition mPosition; + size_t mCursorIdx; +}; + +class TextUndoCommandRemove : public TextUndoCommand { + public: + TextUndoCommandRemove( const Uint64& id, const size_t& cursorIdx, const TextRange& range, + const Time& timestamp ); + + const TextRange& getRange() const; + + size_t getCursorIdx() const; + + json toJSON() { + auto j = baseJSON(); + j["range"] = mRange.toString(); + j["cursorIdx"] = mCursorIdx; + return j; + } + + static TextUndoCommandRemove* fromJSON( json j, Uint64 id ) { + auto timestamp = Time::fromString( j["timestamp"].get() ); + auto range = TextRange::fromString( j["range"].get() ); + auto cursorIdx = j["cursorIdx"].get(); + return eeNew( TextUndoCommandRemove, ( id, cursorIdx, range, timestamp ) ); + } + + protected: + TextRange mRange; + size_t mCursorIdx; +}; + +class TextUndoCommandSelection : public TextUndoCommand { + public: + TextUndoCommandSelection( const Uint64& id, const size_t& cursorIdx, + const TextRanges& selection, const Time& timestamp ); + + const TextRanges& getSelection() const; + + size_t getCursorIdx() const; + + json toJSON() { + auto j = baseJSON(); + j["range"] = mSelection.toString(); + j["cursorIdx"] = mCursorIdx; + return j; + } + + static TextUndoCommandSelection* fromJSON( json j, Uint64 id ) { + auto timestamp = Time::fromString( j["timestamp"].get() ); + auto range = TextRange::fromString( j["range"].get() ); + auto cursorIdx = j["cursorIdx"].get(); + return eeNew( TextUndoCommandSelection, ( id, cursorIdx, range, timestamp ) ); + } + + protected: + TextRanges mSelection; + size_t mCursorIdx; +}; + TextUndoCommand::TextUndoCommand( const Uint64& id, const TextUndoCommandType& type, const Time& timestamp ) : mId( id ), mType( type ), mTimestamp( timestamp ) {} @@ -200,6 +321,57 @@ Uint64 TextUndoStack::getCurrentChangeId() const { return mUndoStack.back()->getId(); } +std::string TextUndoStack::toJSON( bool inverted ) { + json j = json::array(); + if ( inverted ) { + while ( hasUndo() ) + undo(); + + for ( auto it = mRedoStack.rbegin(); it != mRedoStack.rend(); it++ ) { + auto cmd = *it; + j.push_back( cmd->toJSON() ); + } + + while ( hasRedo() ) + redo(); + } else { + for ( auto it = mUndoStack.rbegin(); it != mUndoStack.rend(); it++ ) { + auto cmd = *it; + j.push_back( cmd->toJSON() ); + } + } + return j.dump(); +} + +void TextUndoStack::fromJSON( const std::string& jsonString ) { + json j; + try { + j = json::parse( jsonString, nullptr, true, true ); + if ( !j.is_array() ) + return; + for ( auto it = j.rbegin(); it != j.rend(); it++ ) { + const auto& jobj = *it; + auto type = static_cast( jobj["type"].get() ); + switch ( type ) { + case TextUndoCommandType::Insert: + pushUndo( mRedoStack, + TextUndoCommandInsert::fromJSON( jobj, ++mChangeIdCounter ) ); + break; + case TextUndoCommandType::Remove: + pushUndo( mRedoStack, + TextUndoCommandRemove::fromJSON( jobj, ++mChangeIdCounter ) ); + break; + case TextUndoCommandType::Selection: + pushUndo( mRedoStack, + TextUndoCommandSelection::fromJSON( jobj, ++mChangeIdCounter ) ); + break; + } + } + } catch ( const json::exception& e ) { + Log::error( "TextUndoStack::fromJSON - Error parsing json string:\n%s", jsonString ); + } +} + UndoStackContainer& TextUndoStack::getUndoStackContainer() { return mUndoStack; } diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index 12ac88f20..d5da0d807 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -175,6 +175,7 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, workspace.restoreLastSession = ini.getValueB( "workspace", "restore_last_session", false ); workspace.checkForUpdatesAtStartup = ini.getValueB( "workspace", "check_for_updates_at_startup", true ); + workspace.autoSave = ini.getValueB( "workspace", "auto_save", true ); std::map pluginsEnabled; const auto& creators = pluginManager->getDefinitions(); @@ -314,6 +315,7 @@ void AppConfig::save( const std::vector& recentFiles, ini.setValueB( "workspace", "restore_last_session", workspace.restoreLastSession ); ini.setValueB( "workspace", "check_for_updates_at_startup", workspace.checkForUpdatesAtStartup ); + ini.setValueB( "workspace", "auto_save", workspace.autoSave ); const auto& pluginsEnabled = pluginManager->getPluginsEnabled(); for ( const auto& plugin : pluginsEnabled ) @@ -398,7 +400,8 @@ json saveNode( Node* node ) { void AppConfig::saveProject( std::string projectFolder, UICodeEditorSplitter* editorSplitter, const std::string& configPath, const ProjectDocumentConfig& docConfig, - const ProjectBuildConfiguration& buildConfig ) { + const ProjectBuildConfiguration& buildConfig, bool onlyIfNeeded, + bool autoSave ) { FileSystem::dirAddSlashAtEnd( projectFolder ); std::string projectsPath( configPath + "projects" + FileSystem::getOSSlash() ); if ( !FileSystem::fileExists( projectsPath ) ) @@ -426,7 +429,67 @@ void AppConfig::saveProject( std::string projectFolder, UICodeEditorSplitter* ed cfg.setValue( "nodes", "documents", saveNode( editorSplitter->getBaseLayout()->getFirstChild() ).dump() ); cfg.deleteKey( "files" ); - cfg.writeFile(); + if ( onlyIfNeeded ) { + IOStreamString stringFile; + cfg.writeStream( stringFile ); + if ( !FileSystem::fileExists( cfg.path() ) || + MD5::fromString( stringFile.getStream() ) != MD5::fromFile( cfg.path() ) ) { + FileSystem::fileWrite( cfg.path(), stringFile.getStream() ); + } + } else { + cfg.writeFile(); + } + if ( !autoSave ) + return; + std::string statePath( projectsPath + "state" ); + if ( !FileSystem::fileExists( statePath ) && !FileSystem::makeDir( statePath ) ) + return; + std::string projectStatePath( statePath + FileSystem::getOSSlash() + hash.toHexString() ); + if ( !FileSystem::fileExists( projectStatePath ) && !FileSystem::makeDir( projectStatePath ) ) + return; + FileSystem::dirAddSlashAtEnd( projectStatePath ); + nlohmann::json j = nlohmann::json::array(); + std::vector fileNames; + editorSplitter->forEachDocSharedPtr( + [&j, &projectStatePath, &fileNames]( std::shared_ptr doc ) { + if ( !doc->isDirty() ) + return; + nlohmann::json fj; + IOStreamString stream; + doc->save( stream, true ); + std::string hash = MD5::fromString( stream.getStream() ).toHexString(); + std::string cacheFileName = hash + "." + doc->getFilename(); + std::string cachePath = projectStatePath + cacheFileName; + fj["cachepath"] = cachePath; + if ( doc->hasFilepath() ) { + fj["fspath"] = doc->getFilePath(); + fj["fsmtime"] = FileInfo( doc->getFilePath() ).getModificationTime(); + fj["fshash"] = doc->getHashHexString(); + } else { + fj["name"] = doc->getFilename(); + fj["selection"] = doc->getSelections().toString(); + } + j.push_back( std::move( fj ) ); + fileNames.push_back( cacheFileName ); + if ( !FileSystem::fileExists( cachePath ) || + MD5::fromFile( cachePath ) != MD5::fromString( stream.getStream() ) ) { + FileSystem::fileWrite( cachePath, stream.getStream() ); + } + } ); + std::string stateFileName( "state.json" ); + fileNames.push_back( stateFileName ); + std::string projectStateFilePath( projectStatePath + stateFileName ); + if ( j.size() != 0 ) { + std::string stateString( j.dump( 2 ) ); + if ( MD5::fromFile( projectStateFilePath ) != MD5::fromString( stateString ) ) + FileSystem::fileWrite( projectStateFilePath, stateString ); + } else if ( FileSystem::fileExists( projectStateFilePath ) ) { + FileSystem::fileRemove( projectStateFilePath ); + } + auto curFiles = FileSystem::filesGetInPath( projectStatePath ); + for ( const auto& file : curFiles ) + if ( std::find( fileNames.begin(), fileNames.end(), file ) == fileNames.end() ) + FileSystem::fileRemove( projectStatePath + file ); } static void countTotalEditors( json j, size_t& curTotal ) { @@ -460,7 +523,8 @@ void AppConfig::editorLoadedCounter( ecode::App* app ) { } void AppConfig::loadDocuments( UICodeEditorSplitter* editorSplitter, json j, - UITabWidget* curTabWidget, ecode::App* app ) { + UITabWidget* curTabWidget, ecode::App* app, + const std::vector& autoSaveFiles ) { if ( j["type"] == "tabwidget" ) { Int64 currentPage = j["current_page"]; size_t totalToLoad = j["files"].size(); @@ -491,20 +555,45 @@ void AppConfig::loadDocuments( UICodeEditorSplitter* editorSplitter, json j, editorLoadedCounter( app ); } else { + auto autoSaveIt = std::find_if( + autoSaveFiles.begin(), autoSaveFiles.end(), + [&path]( const AutoSaveFile& file ) { return file.fspath == path; } ); + + std::string loadPath( path ); + AutoSaveFile autoSaveFile; + if ( autoSaveIt != autoSaveFiles.end() ) + autoSaveFile = *autoSaveIt; + editorSplitter->loadAsyncFileFromPathInNewTab( path, - [this, curTabWidget, selection, totalToLoad, currentPage, - app]( UICodeEditor* editor, const std::string& ) { + [this, curTabWidget, selection, totalToLoad, currentPage, app, path, + autoSaveFile]( UICodeEditor* editor, const std::string& ) { if ( !editor->getDocument().getSelection().isValid() || editor->getDocument().getSelection() == TextRange( { 0, 0 }, { 0, 0 } ) ) { editor->getDocument().setSelection( selection ); editor->scrollToCursor(); } + if ( curTabWidget->getTabCount() == totalToLoad ) curTabWidget->setTabSelected( eeclamp( currentPage, 0, curTabWidget->getTabCount() - 1 ) ); + if ( !autoSaveFile.cachePath.empty() ) { + TextDocument& doc = editor->getDocument(); + auto diskFileInfo = doc.getFileInfo(); + TextDocument cachedDoc; + cachedDoc.loadFromFile( autoSaveFile.cachePath ); + doc.selectAll(); + doc.textInput( cachedDoc.getText() ); + doc.setSelection( selection ); + doc.resetUndoRedo(); + doc.setDirtyUntilSave(); + editor->scrollToCursor(); + if ( diskFileInfo.getModificationTime() > autoSaveFile.fsmtime ) + app->createDocDirtyAlert( editor, false ); + } + editorLoadedCounter( app ); }, curTabWidget ); @@ -527,9 +616,9 @@ void AppConfig::loadDocuments( UICodeEditorSplitter* editorSplitter, json j, if ( nullptr == splitter ) return; - loadDocuments( editorSplitter, j["first"], curTabWidget, app ); + loadDocuments( editorSplitter, j["first"], curTabWidget, app, autoSaveFiles ); UITabWidget* tabWidget = splitter->getLastWidget()->asType(); - loadDocuments( editorSplitter, j["last"], tabWidget, app ); + loadDocuments( editorSplitter, j["last"], tabWidget, app, autoSaveFiles ); splitter->setSplitPartition( StyleSheetLength( j["split"] ) ); } @@ -537,7 +626,7 @@ void AppConfig::loadDocuments( UICodeEditorSplitter* editorSplitter, json j, void AppConfig::loadProject( std::string projectFolder, UICodeEditorSplitter* editorSplitter, const std::string& configPath, ProjectDocumentConfig& docConfig, - ecode::App* app ) { + ecode::App* app, bool autoSave ) { FileSystem::dirAddSlashAtEnd( projectFolder ); std::string projectsPath( configPath + "projects" + FileSystem::getOSSlash() ); MD5::Result hash = MD5::fromString( projectFolder ); @@ -572,21 +661,72 @@ void AppConfig::loadProject( std::string projectFolder, UICodeEditorSplitter* ed app->getProjectBuildManager()->setConfig( prjCfg ); } + std::vector autoSaveFiles; + if ( autoSave ) { + std::string projectStatePath( projectsPath + "state" + FileSystem::getOSSlash() + + hash.toHexString() + FileSystem::getOSSlash() + + "state.json" ); + if ( FileSystem::fileExists( projectStatePath ) ) { + std::string stateStr; + FileSystem::fileGet( projectStatePath, stateStr ); + json j; + try { + j = json::parse( stateStr ); + if ( !j.is_discarded() && j.is_array() ) { + for ( const auto& jobj : j ) { + AutoSaveFile autoSaveFile; + autoSaveFile.cachePath = jobj.value( "cachepath", "" ); + if ( autoSaveFile.cachePath.empty() ) + continue; + autoSaveFile.fspath = jobj.value( "fspath", "" ); + autoSaveFile.fsmtime = jobj.value( "fsmtime", 0 ); + autoSaveFile.fshash = jobj.value( "fshash", "" ); + autoSaveFile.name = jobj.value( "name", "" ); + autoSaveFile.selection = jobj.value( "selection", "" ); + autoSaveFiles.emplace_back( std::move( autoSaveFile ) ); + } + } + } catch ( const json::exception& e ) { + Log::error( "AppConfig::loadProject: error loading project state: %s", e.what() ); + } + } + } + + UITabWidget* curTabWidget = + editorSplitter->tabWidgetFromWidget( editorSplitter->getCurWidget() ); + if ( cfg.keyValueExists( "nodes", "documents" ) ) { json j; try { j = json::parse( cfg.getValue( "nodes", "documents" ) ); } catch ( const json::exception& e ) { Log::error( "AppConfig::loadProject: error loading project: %s", e.what() ); - return; } - if ( j.is_discarded() ) - return; + if ( !j.is_discarded() ) { + editorsToLoad = countTotalEditors( j ); + loadDocuments( editorSplitter, j, curTabWidget, app, autoSaveFiles ); + } + } - editorsToLoad = countTotalEditors( j ); - UITabWidget* curTabWidget = - editorSplitter->tabWidgetFromWidget( editorSplitter->getCurWidget() ); - loadDocuments( editorSplitter, j, curTabWidget, app ); + for ( const auto& autoSaveFile : autoSaveFiles ) { + if ( !autoSaveFile.fspath.empty() || autoSaveFile.name.empty() || + autoSaveFile.cachePath.empty() ) + continue; + + editorSplitter->loadAsyncFileFromPathInNewTab( + autoSaveFile.cachePath, + [autoSaveFile]( UICodeEditor* editor, const std::string& ) { + TextDocument& doc = editor->getDocument(); + auto selection = TextRange::fromString( autoSaveFile.selection ); + doc.setDefaultFileName( autoSaveFile.name ); + doc.changeFilePath( autoSaveFile.name ); + doc.setDirtyUntilSave(); + doc.setSelection( selection ); + doc.resetUndoRedo(); + doc.setDirtyUntilSave(); + editor->scrollToCursor(); + }, + curTabWidget ); } } diff --git a/src/tools/ecode/appconfig.hpp b/src/tools/ecode/appconfig.hpp index ea25ba170..c68dae823 100644 --- a/src/tools/ecode/appconfig.hpp +++ b/src/tools/ecode/appconfig.hpp @@ -122,7 +122,6 @@ struct ProjectDocumentConfig { }; struct ProjectBuildConfiguration { - ProjectBuildConfiguration() {} std::string buildName; std::string buildType; std::string runName; @@ -166,12 +165,22 @@ struct TerminalConfig { struct WorkspaceConfig { bool restoreLastSession{ false }; bool checkForUpdatesAtStartup{ true }; + bool autoSave{ true }; }; struct LanguagesExtensions { std::map priorities; }; +struct AutoSaveFile { + std::string cachePath; + std::string fspath; + Uint64 fsmtime{ 0 }; + std::string fshash; + std::string name; + std::string selection; +}; + class AppConfig { public: WindowStateConfig windowState; @@ -203,17 +212,18 @@ class AppConfig { void saveProject( std::string projectFolder, UICodeEditorSplitter* editorSplitter, const std::string& configPath, const ProjectDocumentConfig& docConfig, - const ProjectBuildConfiguration& buildConfig ); + const ProjectBuildConfiguration& buildConfig, bool onlyIfNeeded, + bool autoSave ); void loadProject( std::string projectFolder, UICodeEditorSplitter* editorSplitter, const std::string& configPath, ProjectDocumentConfig& docConfig, - ecode::App* app ); + ecode::App* app, bool autoSave ); protected: Int64 editorsToLoad{ 0 }; void loadDocuments( UICodeEditorSplitter* editorSplitter, json j, UITabWidget* curTabWidget, - ecode::App* app ); + ecode::App* app, const std::vector& autoSaveFiles ); void editorLoadedCounter( ecode::App* app ); }; diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 430cedce9..7a2bd3498 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -54,7 +54,7 @@ void appLoop() { } bool App::onCloseRequestCallback( EE::Window::Window* ) { - if ( mSplitter->isAnyEditorDirty() ) { + if ( mSplitter->isAnyEditorDirty() && !mConfig.workspace.autoSave ) { if ( mCloseMsgBox ) return false; mCloseMsgBox = UIMessageBox::New( @@ -2200,11 +2200,12 @@ bool App::isUnlockedCommand( const std::string& command ) { return std::find( cmds.begin(), cmds.end(), command ) != cmds.end(); } -void App::saveProject() { +void App::saveProject( bool onlyIfNeeded, bool autoSaveEnabled ) { if ( !mCurrentProject.empty() ) { mConfig.saveProject( mCurrentProject, mSplitter, mConfigPath, mProjectDocConfig, mProjectBuildManager ? mProjectBuildManager->getConfig() - : ProjectBuildConfiguration() ); + : ProjectBuildConfiguration(), + onlyIfNeeded, autoSaveEnabled && mConfig.workspace.autoSave ); } } @@ -2258,10 +2259,12 @@ void App::closeFolder() { if ( mCurrentProject.empty() ) return; + saveProject( true ); + if ( mProjectBuildManager ) mProjectBuildManager.reset(); - if ( mSplitter->isAnyEditorDirty() ) { + if ( mSplitter->isAnyEditorDirty() && !mConfig.workspace.autoSave ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, i18n( "confirm_close_folder", @@ -2284,7 +2287,7 @@ void App::closeFolder() { } } -void App::createDocDirtyAlert( UICodeEditor* editor ) { +void App::createDocDirtyAlert( UICodeEditor* editor, bool showEnableAutoReload ) { UILinearLayout* docAlert = editor->findByClass( "doc_alert" ); if ( docAlert ) @@ -2310,7 +2313,7 @@ void App::createDocDirtyAlert( UICodeEditor* editor ) { editor->enableReportSizeChangeToChilds(); docAlert->find( "file_autoreload" ) - ->setVisible( !editor->getDocument().isDirty() ) + ->setVisible( showEnableAutoReload ? !editor->getDocument().isDirty() : false ) ->onClick( [editor, docAlert, this]( const MouseEvent* ) { editor->getDocument().reload(); editor->disableReportSizeChangeToChilds(); @@ -2688,6 +2691,11 @@ void App::onCodeEditorCreated( UICodeEditor* editor, TextDocument& doc ) { UICodeEditor* editor = event->getNode()->asType(); editor->runOnMainThread( [this, editor] { updateEditorTabTitle( editor ); + + UITab* tab = reinterpret_cast( editor->getData() ); + tab->setTooltipText( + editor->getDocument().hasFilepath() ? editor->getDocument().getFilePath() : "" ); + editor->setSyntaxDefinition( editor->getDocument().guessSyntax() ); } ); } ); @@ -3345,7 +3353,8 @@ void App::loadFolder( const std::string& path ) { Clock projClock; mProjectBuildManager = std::make_unique( rpath, mThreadPool, mSidePanel, this ); - mConfig.loadProject( rpath, mSplitter, mConfigPath, mProjectDocConfig, this ); + mConfig.loadProject( rpath, mSplitter, mConfigPath, mProjectDocConfig, this, + mConfig.workspace.autoSave ); Log::info( "Load project took: %.2f ms", projClock.getElapsedTime().asMilliseconds() ); loadFileSystemMatcher( rpath ); @@ -3379,7 +3388,7 @@ void App::loadFolder( const std::string& path ) { mPluginManager->setWorkspaceFolder( rpath ); - saveProject(); + saveProject( true, false ); } #if EE_PLATFORM == EE_PLATFORM_MACOS @@ -3895,7 +3904,7 @@ void App::init( const LogLevel& logLevel, std::string file, const Float& pidelDe if ( mWindow && mThreadPool && mWindow->getInput()->getElapsedSinceLastKeyboardOrMouseEvent().asSeconds() < 60.f ) { - saveProject(); + saveProject( true ); #if EE_PLATFORM == EE_PLATFORM_LINUX mThreadPool->run( [] { malloc_trim( 0 ); } ); #endif diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 630d9356b..19816de76 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -457,7 +457,7 @@ class App : public UICodeEditorSplitter::Client { const std::string& getFileToOpen() const; - void saveProject(); + void saveProject( bool onlyIfNeeded = false, bool autoSaveEnabled = true ); std::pair generateConfigPath(); @@ -465,6 +465,8 @@ class App : public UICodeEditorSplitter::Client { bool isAnyStatusBarSectionVisible() const; + void createDocDirtyAlert( UICodeEditor* editor, bool showEnableAutoReload = true ); + protected: std::vector mArgs; EE::Window::Window* mWindow{ nullptr }; @@ -623,8 +625,6 @@ class App : public UICodeEditorSplitter::Client { void removeFolderWatches(); - void createDocDirtyAlert( UICodeEditor* editor ); - void createDocManyLangsAlert( UICodeEditor* editor ); void syncProjectTreeWithEditor( UICodeEditor* editor ); diff --git a/src/tools/ecode/settingsmenu.cpp b/src/tools/ecode/settingsmenu.cpp index 8422b58be..5ea1432c1 100644 --- a/src/tools/ecode/settingsmenu.cpp +++ b/src/tools/ecode/settingsmenu.cpp @@ -640,6 +640,16 @@ UIMenu* SettingsMenu::createDocumentMenu() { mApp->getConfig().editor.autoReloadOnDiskChange ) ->setId( "autoreload_on_disk_change" ); + mGlobalMenu + ->addCheckBox( i18n( "auto_save_documents", "Auto-Save Open Documents" ), + mApp->getConfig().workspace.autoSave ) + ->setTooltipText( + i18n( "auto_save_documents_desc", + "When auto-save open documents is enabled the editor will keep\n" + "the document buffer changes between sessions, even if they are not saved\n" + "before exiting the program." ) ) + ->setId( "auto_save_documents" ); + mGlobalMenu->addSeparator(); mGlobalMenu->add( i18n( "line_breaking_column", "Line Breaking Column" ) ) @@ -668,6 +678,8 @@ UIMenu* SettingsMenu::createDocumentMenu() { mApp->getConfig().doc.autoDetectIndentType = item->isActive(); } else if ( "autoreload_on_disk_change" == id ) { mApp->getConfig().editor.autoReloadOnDiskChange = item->isActive(); + } else if ( "auto_save_documents" == id ) { + mApp->getConfig().workspace.autoSave = item->isActive(); } } else if ( "line_breaking_column" == id ) { mApp->setLineBreakingColumn();