Skip to main content

strop_engine/editor/io/
codec.rs

1//! Pure completion data crosses replay; parsers, clients and leases never do.
2use super::Opened;
3use crate::editor::document::{DocumentSource, RemoteDirectory, RemoteDocument, ReturnPoint};
4use crate::editor::Document;
5use crate::files::FileTarget;
6use serde::{Deserialize, Serialize};
7use strop_remote::{ReadSelection, RemoteEntry, RemoteWindow};
8
9#[derive(Deserialize)]
10#[serde(tag = "kind")]
11enum RemoteRecord {
12    File {
13        window: RemoteWindow,
14        selection: ReadSelection,
15        return_to: Option<ReturnPoint>,
16    },
17    Directory {
18        entries: Vec<RemoteEntry>,
19        visible: Vec<usize>,
20        filter: String,
21        return_to: Option<ReturnPoint>,
22    },
23}
24#[derive(Deserialize)]
25struct Record {
26    buffer: strop_core::BufferSeed,
27    canonical: FileTarget,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    remote: Option<RemoteRecord>,
30}
31
32#[derive(Serialize)]
33#[serde(tag = "kind")]
34enum RemoteRecordRef<'a> {
35    File {
36        window: RemoteWindow,
37        selection: ReadSelection,
38        return_to: &'a Option<ReturnPoint>,
39    },
40    Directory {
41        entries: &'a [RemoteEntry],
42        visible: &'a [usize],
43        filter: &'a str,
44        return_to: &'a Option<ReturnPoint>,
45    },
46}
47#[derive(Serialize)]
48struct RecordRef<'a> {
49    buffer: strop_core::BufferSeed,
50    canonical: &'a FileTarget,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    remote: Option<RemoteRecordRef<'a>>,
53}
54impl Serialize for Opened {
55    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
56        let remote = match &self.document.source {
57            DocumentSource::Remote(source) => Some(RemoteRecordRef::File {
58                window: source.window,
59                selection: source.selection,
60                return_to: &source.return_to,
61            }),
62            DocumentSource::RemoteDirectory(source) => Some(RemoteRecordRef::Directory {
63                entries: &source.entries,
64                visible: &source.visible,
65                filter: &source.filter,
66                return_to: &source.return_to,
67            }),
68            _ => None,
69        };
70        RecordRef {
71            buffer: self.document.buf.seed(),
72            canonical: &self.canonical,
73            remote,
74        }
75        .serialize(serializer)
76    }
77}
78impl<'de> Deserialize<'de> for Opened {
79    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
80        let record = Record::deserialize(deserializer)?;
81        let buffer = record
82            .buffer
83            .into_buffer()
84            .map_err(serde::de::Error::custom)?;
85        let document = match (&record.canonical, record.remote) {
86            (FileTarget::Local(_), None) => Document::new(buffer),
87            (FileTarget::Remote(location), Some(metadata)) => {
88                if buffer.path.is_some() || buffer.dirty {
89                    return Err(serde::de::Error::custom(
90                        "remote completion has local or dirty content",
91                    ));
92                }
93                let file = location.absolute_file().cloned().ok_or_else(|| {
94                    serde::de::Error::custom(
95                        "remote completion must have a canonical absolute identity",
96                    )
97                })?;
98                match metadata {
99                    RemoteRecord::File {
100                        window,
101                        selection,
102                        return_to,
103                    } => {
104                        if window.length().get() != buffer.len_bytes() as u64
105                            || window
106                                .start()
107                                .get()
108                                .checked_add(window.length().get())
109                                .is_none_or(|end| end > window.file_size().get())
110                        {
111                            return Err(serde::de::Error::custom(
112                                "remote window does not match buffer bytes",
113                            ));
114                        }
115                        Document::remote(
116                            buffer,
117                            RemoteDocument {
118                                file,
119                                window,
120                                selection,
121                                connection: None,
122                                return_to,
123                                write: None,
124                            },
125                        )
126                    }
127                    RemoteRecord::Directory {
128                        entries,
129                        visible,
130                        filter,
131                        return_to,
132                    } => {
133                        let mut indices = std::collections::HashSet::new();
134                        if visible
135                            .iter()
136                            .any(|&index| index >= entries.len() || !indices.insert(index))
137                            || entries.iter().any(|entry| {
138                                entry.file.endpoint() != file.endpoint()
139                                    || entry.file.path().parent() != Some(file.path())
140                            })
141                        {
142                            return Err(serde::de::Error::custom(
143                                "invalid remote directory entry mapping",
144                            ));
145                        }
146                        let source = RemoteDirectory {
147                            directory: file,
148                            entries: entries.into(),
149                            visible,
150                            filter,
151                            connection: None,
152                            return_to,
153                        };
154                        if buffer.text() != source.text().as_str() {
155                            return Err(serde::de::Error::custom(
156                                "directory rows disagree with their native targets",
157                            ));
158                        }
159                        Document::directory(buffer, source)
160                    }
161                }
162            }
163            _ => {
164                return Err(serde::de::Error::custom(
165                    "open completion source metadata mismatch; use the recording version",
166                ))
167            }
168        };
169        Ok(Self {
170            document,
171            canonical: record.canonical,
172        })
173    }
174}