mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-27 08:37:58 +03:00
Auto-Save implementation for project files (issue SpartanJ/ecode#220). Still pending Auto-Save for non-open project environment.
This commit is contained in:
@@ -23,10 +23,6 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#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();
|
||||
|
||||
|
||||
@@ -631,6 +631,14 @@ class EE_API TextDocument {
|
||||
|
||||
void setLines( std::vector<TextDocumentLine>&& 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<std::string, DocumentCommand> mCommands;
|
||||
UnorderedMap<std::string, DocumentRefCommand> 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 {
|
||||
|
||||
@@ -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<TextUndoCommand*>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+15
-15
@@ -8,6 +8,10 @@
|
||||
#include <eepp/system/log.hpp>
|
||||
#include <eepp/system/packmanager.hpp>
|
||||
|
||||
#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;
|
||||
|
||||
@@ -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<TextDocumentLine>&& 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<Uint64>::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 ) {
|
||||
|
||||
@@ -1,11 +1,132 @@
|
||||
#include <eepp/core/core.hpp>
|
||||
#include <eepp/system/log.hpp>
|
||||
#include <eepp/ui/doc/textdocument.hpp>
|
||||
#include <eepp/ui/doc/textundostack.hpp>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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<std::string>() );
|
||||
auto text = String::fromUtf8( j["text"].get<std::string>() );
|
||||
auto position = TextPosition::fromString( j["position"].get<std::string>() );
|
||||
auto cursorIdx = j["cursorIdx"].get<size_t>();
|
||||
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<std::string>() );
|
||||
auto range = TextRange::fromString( j["range"].get<std::string>() );
|
||||
auto cursorIdx = j["cursorIdx"].get<size_t>();
|
||||
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<std::string>() );
|
||||
auto range = TextRange::fromString( j["range"].get<std::string>() );
|
||||
auto cursorIdx = j["cursorIdx"].get<size_t>();
|
||||
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<TextUndoCommandType>( jobj["type"].get<int>() );
|
||||
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;
|
||||
}
|
||||
|
||||
+155
-15
@@ -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<std::string, bool> pluginsEnabled;
|
||||
const auto& creators = pluginManager->getDefinitions();
|
||||
@@ -314,6 +315,7 @@ void AppConfig::save( const std::vector<std::string>& 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<std::string> fileNames;
|
||||
editorSplitter->forEachDocSharedPtr(
|
||||
[&j, &projectStatePath, &fileNames]( std::shared_ptr<TextDocument> 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<AutoSaveFile>& 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<Int32>(
|
||||
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<UITabWidget>();
|
||||
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<AutoSaveFile> 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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<std::string, std::string> 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<AutoSaveFile>& autoSaveFiles );
|
||||
|
||||
void editorLoadedCounter( ecode::App* app );
|
||||
};
|
||||
|
||||
@@ -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<UILinearLayout>( "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<UICodeEditor>();
|
||||
editor->runOnMainThread( [this, editor] {
|
||||
updateEditorTabTitle( editor );
|
||||
|
||||
UITab* tab = reinterpret_cast<UITab*>( 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<ProjectBuildManager>( 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
|
||||
|
||||
@@ -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<bool, std::string> generateConfigPath();
|
||||
|
||||
@@ -465,6 +465,8 @@ class App : public UICodeEditorSplitter::Client {
|
||||
|
||||
bool isAnyStatusBarSectionVisible() const;
|
||||
|
||||
void createDocDirtyAlert( UICodeEditor* editor, bool showEnableAutoReload = true );
|
||||
|
||||
protected:
|
||||
std::vector<std::string> 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 );
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user