Skip to content

Commit 6c5e3c9

Browse files
Refactor persistence to combine document handling and workspace layout (#4031)
* Unify editor state persistence * Review * Fix * Remove redundant DocumentDetails * LoadDocumentContent indirection
1 parent 2a2a608 commit 6c5e3c9

36 files changed

Lines changed: 546 additions & 683 deletions

desktop/src/app.rs

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@ use crate::cef;
1717
use crate::cli::Cli;
1818
use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS;
1919
use crate::event::{AppEvent, AppEventScheduler};
20-
use crate::persist::PersistentData;
20+
use crate::persist;
2121
use crate::preferences;
2222
use crate::render::{RenderError, RenderState};
2323
use crate::window::Window;
24-
use crate::workspace_layout;
2524
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences};
2625
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
2726

@@ -46,7 +45,6 @@ pub(crate) struct App {
4645
start_render_sender: SyncSender<()>,
4746
web_communication_initialized: bool,
4847
web_communication_startup_buffer: Vec<Vec<u8>>,
49-
persistent_data: PersistentData,
5048
#[cfg_attr(not(target_os = "macos"), expect(unused))]
5149
preferences: Preferences,
5250
cli: Cli,
@@ -93,9 +91,6 @@ impl App {
9391
}
9492
});
9593

96-
let mut persistent_data = PersistentData::default();
97-
persistent_data.load_from_disk();
98-
9994
let desktop_wrapper = DesktopWrapper::new(rand::rng().random());
10095

10196
Self {
@@ -119,7 +114,6 @@ impl App {
119114
start_render_sender,
120115
web_communication_initialized: false,
121116
web_communication_startup_buffer: Vec::new(),
122-
persistent_data,
123117
preferences,
124118
cli,
125119
startup_time: None,
@@ -285,17 +279,24 @@ impl App {
285279
window.request_redraw();
286280
}
287281
}
288-
DesktopFrontendMessage::PersistenceWriteDocument { id, document } => {
289-
self.persistent_data.write_document(id, document);
282+
DesktopFrontendMessage::PersistenceWriteState { state } => {
283+
persist::write_state(state);
290284
}
291-
DesktopFrontendMessage::PersistenceDeleteDocument { id } => {
292-
self.persistent_data.delete_document(&id);
285+
DesktopFrontendMessage::PersistenceReadState => {
286+
responses.push(DesktopWrapperMessage::LoadPersistedState { state: persist::read_state() });
293287
}
294-
DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id } => {
295-
self.persistent_data.set_current_document(id);
288+
DesktopFrontendMessage::PersistenceReadDocument { id } => {
289+
if let Some(document) = persist::read_document_content(&id) {
290+
responses.push(DesktopWrapperMessage::LoadDocumentContent { id, document });
291+
} else {
292+
tracing::error!("Failed to read document content for {id:?}");
293+
}
296294
}
297-
DesktopFrontendMessage::PersistenceUpdateDocumentsList { ids } => {
298-
self.persistent_data.force_document_order(ids);
295+
DesktopFrontendMessage::PersistenceWriteDocument { id, document_serialized_content } => {
296+
persist::write_document_content(id, document_serialized_content);
297+
}
298+
DesktopFrontendMessage::PersistenceDeleteDocument { id } => {
299+
persist::delete_document(&id);
299300
}
300301
DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
301302
preferences::write(preferences);
@@ -305,30 +306,6 @@ impl App {
305306
let message = DesktopWrapperMessage::LoadPreferences { preferences };
306307
responses.push(message);
307308
}
308-
DesktopFrontendMessage::PersistenceWriteWorkspaceLayout { workspace_layout: layout } => {
309-
workspace_layout::write(&layout);
310-
}
311-
DesktopFrontendMessage::PersistenceLoadWorkspaceLayout => {
312-
if let Some(workspace_layout) = workspace_layout::read() {
313-
let message = DesktopWrapperMessage::LoadWorkspaceLayout { workspace_layout };
314-
responses.push(message);
315-
}
316-
}
317-
DesktopFrontendMessage::PersistenceLoadDocuments => {
318-
// Open all documents in persisted tab order, then select the current one
319-
for (id, document) in self.persistent_data.documents() {
320-
responses.push(DesktopWrapperMessage::LoadDocument {
321-
id,
322-
document,
323-
to_front: false,
324-
select_after_open: false,
325-
});
326-
}
327-
328-
if let Some(id) = self.persistent_data.current_document_id() {
329-
responses.push(DesktopWrapperMessage::SelectDocument { id });
330-
}
331-
}
332309
DesktopFrontendMessage::OpenLaunchDocuments => {
333310
if self.cli.files.is_empty() {
334311
return;

desktop/src/consts.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ pub(crate) const APP_DIRECTORY_NAME: &str = "Graphite";
99
pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock";
1010
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
1111
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
12-
pub(crate) const APP_WORKSPACE_LAYOUT_FILE_NAME: &str = "workspace_layout.ron";
1312
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
1413

1514
// CEF configuration constants

desktop/src/dirs.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,11 @@ impl AsRef<Path> for TempDir {
8484
&self.path
8585
}
8686
}
87+
88+
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
89+
pub(crate) fn delete_old_cef_browser_directory() {
90+
let old_browser_dir = crate::dirs::app_data_dir().join("browser");
91+
if old_browser_dir.is_dir() {
92+
let _ = std::fs::remove_dir_all(&old_browser_dir);
93+
}
94+
}

desktop/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ mod persist;
2020
mod preferences;
2121
mod render;
2222
mod window;
23-
mod workspace_layout;
2423

2524
pub(crate) mod consts;
2625

@@ -65,6 +64,9 @@ pub fn start() {
6564

6665
dirs::app_tmp_dir_cleanup();
6766

67+
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
68+
dirs::delete_old_cef_browser_directory();
69+
6870
let prefs = preferences::read();
6971

7072
// Must be called before event loop initialization or native window integrations will break

desktop/src/persist.rs

Lines changed: 69 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,169 +1,92 @@
1-
use crate::wrapper::messages::{Document, DocumentId, PersistedDocumentInfo};
2-
3-
#[derive(Default, serde::Serialize, serde::Deserialize)]
4-
pub(crate) struct PersistentData {
5-
documents: Vec<PersistedDocumentInfo>,
6-
current_document: Option<DocumentId>,
7-
#[serde(skip)]
8-
document_order: Option<Vec<DocumentId>>,
9-
}
10-
11-
impl PersistentData {
12-
pub(crate) fn write_document(&mut self, id: DocumentId, document: Document) {
13-
let info = PersistedDocumentInfo {
14-
id,
15-
name: document.name.clone(),
16-
path: document.path.clone(),
17-
is_saved: document.is_saved,
18-
};
19-
if let Some(existing) = self.documents.iter_mut().find(|doc| doc.id == id) {
20-
*existing = info;
21-
} else {
22-
self.documents.push(info);
1+
use crate::wrapper::messages::{DocumentId, PersistedState};
2+
3+
pub(crate) fn read_state() -> PersistedState {
4+
let path = state_file_path();
5+
let data = match std::fs::read_to_string(&path) {
6+
Ok(d) => d,
7+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
8+
tracing::info!("No persistent data file found at {path:?}, starting fresh");
9+
return PersistedState::default();
2310
}
24-
25-
if let Err(e) = std::fs::write(Self::document_content_path(&id), document.content) {
26-
tracing::error!("Failed to write document {id:?} to disk: {e}");
11+
Err(e) => {
12+
tracing::error!("Failed to read persistent data from disk: {e}");
13+
return PersistedState::default();
2714
}
28-
29-
self.flush();
30-
}
31-
32-
pub(crate) fn delete_document(&mut self, id: &DocumentId) {
33-
if Some(*id) == self.current_document {
34-
self.current_document = None;
35-
}
36-
37-
self.documents.retain(|doc| doc.id != *id);
38-
if let Err(e) = std::fs::remove_file(Self::document_content_path(id)) {
39-
tracing::error!("Failed to delete document {id:?} from disk: {e}");
15+
};
16+
let loaded = match ron::from_str(&data) {
17+
Ok(d) => d,
18+
Err(e) => {
19+
tracing::error!("Failed to deserialize persistent data: {e}");
20+
return PersistedState::default();
4021
}
22+
};
4123

42-
self.flush();
43-
}
24+
garbage_collect_document_files(&loaded);
25+
loaded
26+
}
4427

45-
pub(crate) fn current_document_id(&self) -> Option<DocumentId> {
46-
match self.current_document {
47-
Some(id) => Some(id),
48-
None => Some(self.documents.first()?.id),
28+
pub(crate) fn write_state(state: PersistedState) {
29+
let state: &PersistedState = &state;
30+
let data = match ron::ser::to_string_pretty(state, Default::default()) {
31+
Ok(d) => d,
32+
Err(e) => {
33+
tracing::error!("Failed to serialize persistent data: {e}");
34+
return;
4935
}
36+
};
37+
if let Err(e) = std::fs::write(state_file_path(), data) {
38+
tracing::error!("Failed to write persistent data to disk: {e}");
5039
}
40+
garbage_collect_document_files(&state);
41+
}
5142

52-
pub(crate) fn documents(&self) -> Vec<(DocumentId, Document)> {
53-
self.documents.iter().filter_map(|doc| Some((doc.id, self.read_document(&doc.id)?))).collect()
43+
pub(crate) fn write_document_content(id: DocumentId, document_content: String) {
44+
if let Err(e) = std::fs::write(document_content_path(&id), document_content) {
45+
tracing::error!("Failed to write document {id:?} to disk: {e}");
5446
}
47+
}
5548

56-
pub(crate) fn set_current_document(&mut self, id: DocumentId) {
57-
self.current_document = Some(id);
58-
self.flush();
59-
}
49+
pub(crate) fn read_document_content(id: &DocumentId) -> Option<String> {
50+
std::fs::read_to_string(document_content_path(id)).ok()
51+
}
6052

61-
pub(crate) fn force_document_order(&mut self, order: Vec<DocumentId>) {
62-
let mut ordered_prefix_length = 0;
63-
for id in &order {
64-
if let Some(offset) = self.documents[ordered_prefix_length..].iter().position(|doc| doc.id == *id) {
65-
let found_index = ordered_prefix_length + offset;
66-
if found_index != ordered_prefix_length {
67-
self.documents[ordered_prefix_length..=found_index].rotate_right(1);
68-
}
69-
ordered_prefix_length += 1;
70-
}
71-
}
72-
self.document_order = Some(order);
73-
self.flush();
53+
pub(crate) fn delete_document(id: &DocumentId) {
54+
if let Err(e) = std::fs::remove_file(document_content_path(id)) {
55+
tracing::error!("Failed to delete document {id:?} from disk: {e}");
7456
}
57+
}
7558

76-
fn read_document(&self, id: &DocumentId) -> Option<Document> {
77-
let info = self.documents.iter().find(|doc| doc.id == *id)?;
78-
let content = std::fs::read_to_string(Self::document_content_path(id)).ok()?;
79-
Some(Document {
80-
content,
81-
name: info.name.clone(),
82-
path: info.path.clone(),
83-
is_saved: info.is_saved,
84-
})
85-
}
59+
fn garbage_collect_document_files(state: &PersistedState) {
60+
let valid_paths: std::collections::HashSet<_> = state.documents.iter().map(|doc| document_content_path(&doc.id)).collect();
8661

87-
fn flush(&self) {
88-
let data = match ron::ser::to_string_pretty(self, Default::default()) {
89-
Ok(d) => d,
90-
Err(e) => {
91-
tracing::error!("Failed to serialize persistent data: {e}");
92-
return;
93-
}
94-
};
95-
if let Err(e) = std::fs::write(Self::state_file_path(), data) {
96-
tracing::error!("Failed to write persistent data to disk: {e}");
62+
let directory = crate::dirs::app_autosave_documents_dir();
63+
let entries = match std::fs::read_dir(&directory) {
64+
Ok(entries) => entries,
65+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
66+
Err(e) => {
67+
tracing::error!("Failed to read autosave documents directory: {e}");
68+
return;
9769
}
98-
}
99-
100-
pub(crate) fn load_from_disk(&mut self) {
101-
delete_old_cef_browser_directory();
102-
103-
let path = Self::state_file_path();
104-
let data = match std::fs::read_to_string(&path) {
105-
Ok(d) => d,
106-
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
107-
tracing::info!("No persistent data file found at {path:?}, starting fresh");
108-
return;
109-
}
110-
Err(e) => {
111-
tracing::error!("Failed to read persistent data from disk: {e}");
112-
return;
113-
}
114-
};
115-
let loaded = match ron::from_str(&data) {
116-
Ok(d) => d,
117-
Err(e) => {
118-
tracing::error!("Failed to deserialize persistent data: {e}");
119-
return;
120-
}
121-
};
122-
*self = loaded;
123-
124-
self.garbage_collect_document_files();
125-
}
126-
127-
fn garbage_collect_document_files(&self) {
128-
let valid_paths: std::collections::HashSet<_> = self.documents.iter().map(|doc| Self::document_content_path(&doc.id)).collect();
70+
};
12971

130-
let directory = crate::dirs::app_autosave_documents_dir();
131-
let entries = match std::fs::read_dir(&directory) {
132-
Ok(entries) => entries,
133-
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
134-
Err(e) => {
135-
tracing::error!("Failed to read autosave documents directory: {e}");
136-
return;
137-
}
138-
};
139-
140-
for entry in entries.flatten() {
141-
let path = entry.path();
142-
if path.is_file() && !valid_paths.contains(&path) {
143-
if let Err(e) = std::fs::remove_file(&path) {
144-
tracing::error!("Failed to remove orphaned document file {path:?}: {e}");
145-
}
72+
for entry in entries.flatten() {
73+
let path = entry.path();
74+
if path.is_file() && !valid_paths.contains(&path) {
75+
if let Err(e) = std::fs::remove_file(&path) {
76+
tracing::error!("Failed to remove orphaned document file {path:?}: {e}");
14677
}
14778
}
14879
}
80+
}
14981

150-
fn state_file_path() -> std::path::PathBuf {
151-
let mut path = crate::dirs::app_data_dir();
152-
path.push(crate::consts::APP_STATE_FILE_NAME);
153-
path
154-
}
155-
156-
fn document_content_path(id: &DocumentId) -> std::path::PathBuf {
157-
let mut path = crate::dirs::app_autosave_documents_dir();
158-
path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION));
159-
path
160-
}
82+
fn state_file_path() -> std::path::PathBuf {
83+
let mut path = crate::dirs::app_data_dir();
84+
path.push(crate::consts::APP_STATE_FILE_NAME);
85+
path
16186
}
16287

163-
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
164-
fn delete_old_cef_browser_directory() {
165-
let old_browser_dir = crate::dirs::app_data_dir().join("browser");
166-
if old_browser_dir.is_dir() {
167-
let _ = std::fs::remove_dir_all(&old_browser_dir);
168-
}
88+
fn document_content_path(id: &DocumentId) -> std::path::PathBuf {
89+
let mut path = crate::dirs::app_autosave_documents_dir();
90+
path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION));
91+
path
16992
}

desktop/src/workspace_layout.rs

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)