Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 7 additions & 27 deletions desktop/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,39 +314,19 @@ impl App {
responses.push(message);
}
}
DesktopFrontendMessage::PersistenceLoadCurrentDocument => {
if let Some((id, document)) = self.persistent_data.current_document() {
let message = DesktopWrapperMessage::LoadDocument {
id,
document,
to_front: false,
select_after_open: true,
};
responses.push(message);
}
}
DesktopFrontendMessage::PersistenceLoadRemainingDocuments => {
for (id, document) in self.persistent_data.documents_before_current().into_iter().rev() {
let message = DesktopWrapperMessage::LoadDocument {
id,
document,
to_front: true,
select_after_open: false,
};
responses.push(message);
}
for (id, document) in self.persistent_data.documents_after_current() {
let message = DesktopWrapperMessage::LoadDocument {
DesktopFrontendMessage::PersistenceLoadDocuments => {
// Open all documents in persisted tab order, then select the current one
for (id, document) in self.persistent_data.documents() {
responses.push(DesktopWrapperMessage::LoadDocument {
id,
document,
to_front: false,
select_after_open: false,
};
responses.push(message);
});
}

if let Some(id) = self.persistent_data.current_document_id() {
let message = DesktopWrapperMessage::SelectDocument { id };
responses.push(message);
responses.push(DesktopWrapperMessage::SelectDocument { id });
}
}
DesktopFrontendMessage::OpenLaunchDocuments => {
Expand Down
171 changes: 69 additions & 102 deletions desktop/src/persist.rs
Original file line number Diff line number Diff line change
@@ -1,65 +1,56 @@
use crate::wrapper::messages::{Document, DocumentId};
use crate::wrapper::messages::{Document, DocumentId, PersistedDocumentInfo};

#[derive(Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct PersistentData {
documents: DocumentStore,
documents: Vec<PersistedDocumentInfo>,
current_document: Option<DocumentId>,
#[serde(skip)]
document_order: Option<Vec<DocumentId>>,
}

impl PersistentData {
pub(crate) fn write_document(&mut self, id: DocumentId, document: Document) {
self.documents.write(id, document);
if let Some(order) = &self.document_order {
self.documents.force_order(order);
let info = PersistedDocumentInfo {
id,
name: document.name.clone(),
path: document.path.clone(),
is_saved: document.is_saved,
};
if let Some(existing) = self.documents.iter_mut().find(|doc| doc.id == id) {
*existing = info;
} else {
self.documents.push(info);
}

if let Err(e) = std::fs::write(Self::document_content_path(&id), document.content) {
tracing::error!("Failed to write document {id:?} to disk: {e}");
}

self.flush();
}

pub(crate) fn delete_document(&mut self, id: &DocumentId) {
if Some(*id) == self.current_document {
self.current_document = None;
}
self.documents.delete(id);

self.documents.retain(|doc| doc.id != *id);
if let Err(e) = std::fs::remove_file(Self::document_content_path(id)) {
tracing::error!("Failed to delete document {id:?} from disk: {e}");
}

self.flush();
}

pub(crate) fn current_document_id(&self) -> Option<DocumentId> {
match self.current_document {
Some(id) => Some(id),
None => Some(*self.documents.document_ids().first()?),
None => Some(self.documents.first()?.id),
}
}

pub(crate) fn current_document(&self) -> Option<(DocumentId, Document)> {
let current_id = self.current_document_id()?;
Some((current_id, self.documents.read(&current_id)?))
}

pub(crate) fn documents_before_current(&self) -> Vec<(DocumentId, Document)> {
let Some(current_id) = self.current_document_id() else {
return Vec::new();
};
self.documents
.document_ids()
.into_iter()
.take_while(|id| *id != current_id)
.filter_map(|id| Some((id, self.documents.read(&id)?)))
.collect()
}

pub(crate) fn documents_after_current(&self) -> Vec<(DocumentId, Document)> {
let Some(current_id) = self.current_document_id() else {
return Vec::new();
};
self.documents
.document_ids()
.into_iter()
.skip_while(|id| *id != current_id)
.skip(1)
.filter_map(|id| Some((id, self.documents.read(&id)?)))
.collect()
pub(crate) fn documents(&self) -> Vec<(DocumentId, Document)> {
self.documents.iter().filter_map(|doc| Some((doc.id, self.read_document(&doc.id)?))).collect()
}

pub(crate) fn set_current_document(&mut self, id: DocumentId) {
Expand All @@ -68,11 +59,31 @@ impl PersistentData {
}

pub(crate) fn force_document_order(&mut self, order: Vec<DocumentId>) {
let mut ordered_prefix_length = 0;
for id in &order {
if let Some(offset) = self.documents[ordered_prefix_length..].iter().position(|doc| doc.id == *id) {
let found_index = ordered_prefix_length + offset;
if found_index != ordered_prefix_length {
self.documents[ordered_prefix_length..=found_index].rotate_right(1);
}
ordered_prefix_length += 1;
}
}
self.document_order = Some(order);
self.documents.force_order(self.document_order.as_ref().unwrap());
self.flush();
}

fn read_document(&self, id: &DocumentId) -> Option<Document> {
let info = self.documents.iter().find(|doc| doc.id == *id)?;
let content = std::fs::read_to_string(Self::document_content_path(id)).ok()?;
Some(Document {
content,
name: info.name.clone(),
path: info.path.clone(),
is_saved: info.is_saved,
})
}

fn flush(&self) {
let data = match ron::ser::to_string_pretty(self, Default::default()) {
Ok(d) => d,
Expand Down Expand Up @@ -107,86 +118,42 @@ impl PersistentData {
}
};
*self = loaded;
}

fn state_file_path() -> std::path::PathBuf {
let mut path = crate::dirs::app_data_dir();
path.push(crate::consts::APP_STATE_FILE_NAME);
path
self.garbage_collect_document_files();
}
}

#[derive(Default, serde::Serialize, serde::Deserialize)]
struct DocumentStore(Vec<DocumentInfo>);
impl DocumentStore {
fn write(&mut self, id: DocumentId, document: Document) {
let meta = DocumentInfo::new(id, &document);
if let Some(existing) = self.0.iter_mut().find(|meta| meta.id == id) {
*existing = meta;
} else {
self.0.push(meta);
}
if let Err(e) = std::fs::write(Self::document_path(&id), document.content) {
tracing::error!("Failed to write document {id:?} to disk: {e}");
}
}
fn garbage_collect_document_files(&self) {
let valid_paths: std::collections::HashSet<_> = self.documents.iter().map(|doc| Self::document_content_path(&doc.id)).collect();

fn delete(&mut self, id: &DocumentId) {
self.0.retain(|meta| meta.id != *id);
if let Err(e) = std::fs::remove_file(Self::document_path(id)) {
tracing::error!("Failed to delete document {id:?} from disk: {e}");
}
}

fn read(&self, id: &DocumentId) -> Option<Document> {
let meta = self.0.iter().find(|meta| meta.id == *id)?;
let content = std::fs::read_to_string(Self::document_path(id)).ok()?;
Some(Document {
content,
name: meta.name.clone(),
path: meta.path.clone(),
is_saved: meta.is_saved,
})
}
let directory = crate::dirs::app_autosave_documents_dir();
let entries = match std::fs::read_dir(&directory) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
Err(e) => {
tracing::error!("Failed to read autosave documents directory: {e}");
return;
}
};

fn force_order(&mut self, desired_order: &[DocumentId]) {
let mut ordered_prefix_len = 0;
for id in desired_order {
if let Some(offset) = self.0[ordered_prefix_len..].iter().position(|meta| meta.id == *id) {
let found_index = ordered_prefix_len + offset;
if found_index != ordered_prefix_len {
self.0[ordered_prefix_len..=found_index].rotate_right(1);
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && !valid_paths.contains(&path) {
if let Err(e) = std::fs::remove_file(&path) {
tracing::error!("Failed to remove orphaned document file {path:?}: {e}");
}
ordered_prefix_len += 1;
}
}
}

fn document_ids(&self) -> Vec<DocumentId> {
self.0.iter().map(|meta| meta.id).collect()
fn state_file_path() -> std::path::PathBuf {
let mut path = crate::dirs::app_data_dir();
path.push(crate::consts::APP_STATE_FILE_NAME);
path
}

fn document_path(id: &DocumentId) -> std::path::PathBuf {
fn document_content_path(id: &DocumentId) -> std::path::PathBuf {
let mut path = crate::dirs::app_autosave_documents_dir();
path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION));
path
}
}

#[derive(serde::Serialize, serde::Deserialize)]
struct DocumentInfo {
id: DocumentId,
name: String,
path: Option<std::path::PathBuf>,
is_saved: bool,
}
impl DocumentInfo {
fn new(id: DocumentId, Document { name, path, is_saved, .. }: &Document) -> Self {
Self {
id,
name: name.clone(),
path: path.clone(),
is_saved: *is_saved,
}
}
}
9 changes: 3 additions & 6 deletions desktop/wrapper/src/intercept_frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument {
id: document_id,
document: Document {
content: document,
name: details.name,
path: details.path,
content: document,
is_saved: details.is_saved,
},
});
Expand All @@ -95,11 +95,8 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
// Forward this to update the UI
return Some(FrontendMessage::UpdateOpenDocumentsList { open_documents });
}
FrontendMessage::TriggerLoadFirstAutoSaveDocument => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadCurrentDocument);
}
FrontendMessage::TriggerLoadRestAutoSaveDocuments => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadRemainingDocuments);
FrontendMessage::TriggerLoadAutoSaveDocuments => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadDocuments);
}
FrontendMessage::TriggerOpenLaunchDocuments => {
dispatcher.respond(DesktopFrontendMessage::OpenLaunchDocuments);
Expand Down
4 changes: 2 additions & 2 deletions desktop/wrapper/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::path::PathBuf;

pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage;

pub use graphite_editor::messages::frontend::utility_types::{PersistedDocumentInfo, PersistedState};
pub use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
pub use graphite_editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState as MouseState, EditorPosition as Position, MouseKeys};
pub use graphite_editor::messages::prelude::DocumentId;
Expand Down Expand Up @@ -49,8 +50,7 @@ pub enum DesktopFrontendMessage {
PersistenceUpdateCurrentDocument {
id: DocumentId,
},
PersistenceLoadCurrentDocument,
PersistenceLoadRemainingDocuments,
PersistenceLoadDocuments,
PersistenceUpdateDocumentsList {
ids: Vec<DocumentId>,
},
Expand Down
3 changes: 1 addition & 2 deletions editor/src/messages/frontend/frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ pub enum FrontendMessage {
document: String,
details: DocumentDetails,
},
TriggerLoadFirstAutoSaveDocument,
TriggerLoadRestAutoSaveDocuments,
TriggerLoadAutoSaveDocuments,
TriggerOpenLaunchDocuments,
TriggerLoadPreferences,
TriggerLoadWorkspaceLayout,
Expand Down
21 changes: 19 additions & 2 deletions editor/src/messages/frontend/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,29 @@ pub struct OpenDocument {
pub struct DocumentDetails {
pub name: String,
pub path: Option<PathBuf>,
#[serde(rename = "isSaved")]
#[serde(alias = "isSaved")]
pub is_saved: bool,
#[serde(rename = "isAutoSaved")]
#[serde(alias = "isAutoSaved")]
pub is_auto_saved: bool,
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PersistedDocumentInfo {
pub id: DocumentId,
pub name: String,
#[serde(default)]
pub path: Option<PathBuf>,
pub is_saved: bool,
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PersistedState {
pub documents: Vec<PersistedDocumentInfo>,
pub current_document: Option<DocumentId>,
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum MouseCursorIcon {
Expand Down
Loading
Loading