strop_engine/editor/picker/
preview.rs1use std::io::Read;
8use std::path::Path;
9
10use strop_core::worker::{self, CancelReason, Failure, FailureKind, Load, Outcome, Ticket};
11use strop_picker::Payload;
12
13use super::super::Editor;
14use super::{PreviewKey, PreviewResult, PreviewSource};
15
16#[derive(Debug, Clone)]
19pub struct PreparedPreview {
20 pub rope: ropey::Rope,
21}
22impl From<String> for PreparedPreview {
23 fn from(text: String) -> Self {
24 Self {
25 rope: ropey::Rope::from_str(&text),
26 }
27 }
28}
29impl serde::Serialize for PreparedPreview {
30 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
31 serializer.collect_str(&self.rope)
32 }
33}
34impl<'de> serde::Deserialize<'de> for PreparedPreview {
35 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
36 String::deserialize(deserializer).map(Self::from)
37 }
38}
39
40impl Editor {
41 pub fn picker_preview(&mut self) -> Option<(String, Option<usize>, PreviewSource)> {
42 let item = self.picker.as_ref()?.picker.current()?;
43 let (path, focus_line) = match &item.payload {
44 Payload::RemoteDirectory(_)
45 | Payload::RemoteConnect
46 | Payload::Jump { .. }
47 | Payload::SearchOption(_)
48 | Payload::CodeAction(_)
49 | Payload::Container(_)
50 | Payload::IndentChoice(_) => return None,
51 Payload::Buffer(document) => {
52 let name = self.docs.get(*document)?.label(&self.cwd);
53 return Some((name, None, PreviewSource::Buffer(*document)));
54 }
55 Payload::File(path) => (path.clone(), None),
56 Payload::Grep { path, line, .. } => (path.clone(), Some(*line)),
57 Payload::Remote {
62 endpoint,
63 path,
64 line,
65 ..
66 } => {
67 return Some((
68 format!("{endpoint}{}", path.display()),
69 Some(*line),
70 PreviewSource::Failed("remote hit — accept to open".into()),
71 ));
72 }
73 };
74 let full = self.cwd.join(&path);
75 let title = {
78 let name = path
79 .file_name()
80 .map(|n| n.to_string_lossy().into_owned())
81 .unwrap_or_else(|| path.display().to_string());
82 let parent = path
83 .parent()
84 .map(|p| p.display().to_string())
85 .unwrap_or_default();
86 match focus_line {
87 Some(line) => format!("{name}:{line} {parent}"),
88 None => format!("{name} {parent}"),
89 }
90 .trim_end()
91 .to_string()
92 };
93 let target = crate::files::FileTarget::Local(full.clone());
94 if let Some((document, _)) = self
95 .docs
96 .iter()
97 .find(|(_, document)| document.matches_target(&target))
98 {
99 return Some((title, focus_line, PreviewSource::Buffer(document)));
100 }
101 match self.preview_loads.get(&full) {
102 Some(Load::Failed { failure, .. }) => {
103 return Some((
104 title,
105 focus_line,
106 PreviewSource::Failed(failure.message.clone()),
107 ));
108 }
109 Some(Load::Cancelled { reason, .. }) => {
110 return Some((title, focus_line, PreviewSource::Cancelled(*reason)));
111 }
112 _ => {}
113 }
114 if !self.preview_ready(&full) {
115 return Some((title, focus_line, PreviewSource::Loading));
116 }
117 Some((title, focus_line, PreviewSource::Cached(full)))
118 }
119
120 fn preview_ready(&mut self, path: &Path) -> bool {
124 let Some(picker) = self.picker.as_ref().map(|glue| glue.id) else {
125 return false;
126 };
127 let key = PreviewKey {
128 picker,
129 path: path.to_path_buf(),
130 };
131 if self.previews.contains_key(path)
132 && matches!(self.preview_loads.get(path), Some(Load::Ready(owner)) if owner == &key)
133 {
134 return true;
135 }
136 if self
139 .preview_loads
140 .get(path)
141 .is_some_and(|load| load.covers(&key))
142 {
143 return false;
144 }
145 if let Some(Load::Running(old)) = self.preview_loads.get(path).cloned() {
147 if let Some(handle) = self.worker_handles.remove(&old.request) {
148 handle.cancel(CancelReason::Superseded);
149 }
150 }
151 let request = match self.worker_ids.allocate() {
152 Ok(request) => request,
153 Err(error) => {
154 self.message = error.message;
155 return false;
156 }
157 };
158 let ticket = Ticket {
159 request,
160 key: key.clone(),
161 };
162 self.preview_loads
165 .insert(path.to_path_buf(), Load::Running(ticket.clone()));
166 strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
167 serde_json::json!({
168 "service":"preview","request":request.get(),
169 "picker":picker.0.get(),"path":path.to_string_lossy(),
170 })
171 });
172 match self
173 .tape
174 .request("strop-preview", &serde_json::json!({"ticket":ticket}))
175 {
176 Ok(false) => return false,
177 Ok(true) => {}
178 Err(error) => {
179 self.handle_preview(PreviewResult {
180 ticket,
181 outcome: Outcome::failed(FailureKind::Protocol, error.to_string()),
182 });
183 return false;
184 }
185 }
186 let launch_path = path.to_path_buf();
187 let tx = self.preview_tx.clone();
188 let handle = worker::spawn(
189 "strop-preview",
190 move |outcome| {
191 let _ = tx.send(PreviewResult { ticket, outcome });
192 },
193 move |_| read_preview(&launch_path),
194 );
195 self.worker_handles.insert(request, handle);
196 false
197 }
198}
199
200pub(crate) fn read_preview(path: &Path) -> Outcome<PreparedPreview> {
204 const LIMIT: u64 = 512 * 1024;
205 let read = || -> Result<String, Failure> {
206 let meta =
207 std::fs::metadata(path).map_err(|e| Failure::new(FailureKind::Io, e.to_string()))?;
208 if !meta.is_file() {
209 return Err(Failure::new(
210 FailureKind::Unavailable,
211 "preview target is not a file",
212 ));
213 }
214 if meta.len() > LIMIT {
215 return Err(Failure::new(FailureKind::Unavailable, "preview too large"));
216 }
217 let file =
218 std::fs::File::open(path).map_err(|e| Failure::new(FailureKind::Io, e.to_string()))?;
219 let mut bytes = Vec::new();
220 file.take(LIMIT + 1)
221 .read_to_end(&mut bytes)
222 .map_err(|e| Failure::new(FailureKind::Io, e.to_string()))?;
223 if bytes.len() as u64 > LIMIT {
224 return Err(Failure::new(FailureKind::Unavailable, "preview too large"));
225 }
226 String::from_utf8(bytes).map_err(|e| Failure::new(FailureKind::Io, e.to_string()))
227 };
228 match read() {
229 Ok(text) => Outcome::Success(text.into()),
230 Err(failure) => Outcome::Failed {
231 failure,
232 partial: None,
233 },
234 }
235}