strop_engine/editor/trace/
seed.rs1use std::io;
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10use strop_core::id::{Arena, ArenaSeed, DocumentId};
11use strop_core::worker::{Load, WorkerId, WorkerIds};
12use strop_core::{Buffer, BufferSeed};
13
14use crate::editor::document::DocumentSource;
15use crate::editor::{Document, Editor, LayoutDir, Pane};
16
17const SEMANTIC_VERSION: u32 = 1;
20
21#[derive(Serialize, Deserialize)]
24struct DocSeed {
25 buffer: BufferSeed,
26 file: bool,
27}
28
29#[derive(Serialize, Deserialize)]
30pub struct Seed {
31 #[serde(default)]
32 semantic_version: u32,
33 documents: ArenaSeed<DocSeed>,
34 panes: Vec<Pane>,
35 mru: Vec<DocumentId>,
36 active: usize,
37 layout: LayoutDir,
38 #[serde(with = "strop_core::path_serde")]
39 cwd: PathBuf,
40 #[serde(with = "strop_core::path_serde::option")]
41 state_dir: Option<PathBuf>,
42 session_policy: crate::session::SessionPolicy,
43 config: crate::config::Config,
44 git: Option<strop_git::GitContext>,
45 git_view: WorkerId,
46 git_discovery: Load<crate::editor::git_memory::ContextKey>,
47 worker_ids: WorkerIds,
48 focus_epoch: u64,
49 generation: u64,
50 message: String,
51}
52
53impl Seed {
54 fn validate_semantics(&self) -> io::Result<()> {
55 if self.semantic_version != SEMANTIC_VERSION {
56 return Err(io::Error::other(format!(
57 "unsupported editor semantics version {}; expected {SEMANTIC_VERSION}; re-record with this version",
58 self.semantic_version)));
59 }
60 Ok(())
61 }
62
63 pub fn capture(editor: &Editor) -> io::Result<Self> {
67 if editor.picker.is_some()
68 || !editor.lsp_servers.is_empty()
69 || matches!(editor.git_discovery, Load::Running(_))
70 || editor.docs.iter().any(|(_, document)| {
71 !matches!(
72 document.source,
73 DocumentSource::File | DocumentSource::Scratch
74 )
75 })
76 {
77 return Err(io::Error::other("seed must precede service startup"));
78 }
79 Ok(Self {
80 semantic_version: SEMANTIC_VERSION,
81 documents: editor.docs.seed_with(|document| DocSeed {
82 buffer: document.buf.seed(),
83 file: matches!(document.source, DocumentSource::File),
84 }),
85 panes: editor.panes.clone(),
86 mru: editor.mru.clone(),
87 active: editor.active_pane,
88 layout: editor.layout,
89 cwd: editor.cwd.clone(),
90 state_dir: editor.state_dir.clone(),
91 session_policy: editor.session_policy,
92 config: editor.config.clone(),
93 git: editor.git.clone(),
94 git_view: editor.git_view,
95 git_discovery: editor.git_discovery.clone(),
96 worker_ids: editor.worker_ids.clone(),
97 focus_epoch: editor.focus_epoch,
98 generation: editor.generation,
99 message: editor.message.clone(),
100 })
101 }
102
103 pub fn input_text(&self) -> io::Result<&str> {
106 self.validate_semantics()?;
107 let pane = self
108 .panes
109 .get(self.active)
110 .ok_or_else(|| io::Error::other("seed has no active pane"))?;
111 let document = self
112 .documents
113 .slots
114 .get(pane.doc.index())
115 .filter(|(generation, _)| *generation == pane.doc.generation())
116 .and_then(|(_, document)| document.as_ref())
117 .ok_or_else(|| io::Error::other("seed has no active document"))?;
118 Ok(&document.buffer.text)
119 }
120
121 pub fn into_editor(self, tape: std::rc::Rc<strop_trace::replay::Tape>) -> io::Result<Editor> {
124 self.validate_semantics()?;
125 let mut slots = Vec::with_capacity(self.documents.slots.len());
126 for (generation, value) in self.documents.slots {
127 let document = value
128 .map(|seed| {
129 let buffer = seed.buffer.into_buffer().map_err(io::Error::other)?;
130 Ok::<_, io::Error>(if seed.file {
131 Document::new(buffer)
132 } else {
133 Document::scratch(buffer)
134 })
135 })
136 .transpose()?;
137 slots.push((generation, document));
138 }
139 let docs = Arena::from_seed(ArenaSeed {
140 slots,
141 free: self.documents.free,
142 })
143 .map_err(io::Error::other)?;
144 if docs.is_empty()
145 || self.active >= self.panes.len()
146 || self.panes.iter().any(|pane| docs.get(pane.doc).is_none())
147 || self.mru.iter().any(|id| docs.get(*id).is_none())
148 {
149 return Err(io::Error::other("invalid initial document references"));
150 }
151 for pane in &self.panes {
152 let buffer = &docs
153 .get(pane.doc)
154 .ok_or_else(|| io::Error::other("invalid initial document reference"))?
155 .buf;
156 for selection in
157 std::iter::once(pane.sels.primary()).chain(pane.sels.extra_heads().iter().copied())
158 {
159 if selection.anchor > buffer.len_bytes()
160 || selection.head > buffer.len_bytes()
161 || !buffer.is_boundary(selection.anchor)
162 || !buffer.is_boundary(selection.head)
163 {
164 return Err(io::Error::other("invalid initial selection"));
165 }
166 }
167 }
168 let mut editor = Editor::new_in(Buffer::from_text(""), self.cwd);
169 editor.docs = docs;
170 editor.panes = self.panes;
171 editor.mru = self.mru;
172 editor.active_pane = self.active;
173 editor.layout = self.layout;
174 editor.state_dir = self.state_dir;
175 editor.session_policy = self.session_policy;
176 editor.config = self.config;
177 editor.git = self.git;
178 editor.git_view = self.git_view;
179 editor.git_discovery = self.git_discovery;
180 editor.worker_ids = self.worker_ids;
181 editor.focus_epoch = self.focus_epoch;
182 editor.generation = self.generation;
183 editor.message = self.message;
184 editor.tape = tape;
185 Ok(editor)
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn legacy_input_capture_is_not_reinterpreted_as_the_new_query_language() {
195 let editor = Editor::new_in(Buffer::from_text("seed\n"), PathBuf::from("/virtual"));
196 let mut serialized = serde_json::to_value(Seed::capture(&editor).unwrap()).unwrap();
197 serialized
198 .as_object_mut()
199 .unwrap()
200 .remove("semantic_version");
201 let legacy: Seed = serde_json::from_value(serialized).unwrap();
202 assert!(legacy
203 .input_text()
204 .unwrap_err()
205 .to_string()
206 .contains("editor semantics"));
207 }
208}