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::CodeAction(_)
48 | Payload::Container(_) => return None,
49 Payload::Buffer(document) => {
50 let name = self
51 .docs
52 .get(*document)?
53 .buf
54 .path
55 .as_ref()
56 .map(|path| path.to_string_lossy().into_owned())
57 .unwrap_or_else(|| "[scratch]".into());
58 return Some((name, None, PreviewSource::Buffer(*document)));
59 }
60 Payload::File(path) => (path.clone(), None),
61 Payload::Grep { path, line, .. } => (path.clone(), Some(*line)),
62 Payload::Remote {
67 endpoint,
68 path,
69 line,
70 ..
71 } => {
72 return Some((
73 format!("{endpoint}{}", path.display()),
74 Some(*line),
75 PreviewSource::Failed("remote hit — accept to open".into()),
76 ));
77 }
78 };
79 let full = self.cwd.join(&path);
80 let title = {
83 let name = path
84 .file_name()
85 .map(|n| n.to_string_lossy().into_owned())
86 .unwrap_or_else(|| path.display().to_string());
87 let parent = path
88 .parent()
89 .map(|p| p.display().to_string())
90 .unwrap_or_default();
91 match focus_line {
92 Some(line) => format!("{name}:{line} {parent}"),
93 None => format!("{name} {parent}"),
94 }
95 .trim_end()
96 .to_string()
97 };
98 if let Some((document, _)) = self
99 .docs
100 .iter()
101 .find(|(_, document)| document.buf.path.as_ref() == Some(&full))
102 {
103 return Some((title, focus_line, PreviewSource::Buffer(document)));
104 }
105 match self.preview_loads.get(&full) {
106 Some(Load::Failed { failure, .. }) => {
107 return Some((
108 title,
109 focus_line,
110 PreviewSource::Failed(failure.message.clone()),
111 ));
112 }
113 Some(Load::Cancelled { reason, .. }) => {
114 return Some((title, focus_line, PreviewSource::Cancelled(*reason)));
115 }
116 _ => {}
117 }
118 if !self.preview_ready(&full) {
119 return Some((title, focus_line, PreviewSource::Loading));
120 }
121 Some((title, focus_line, PreviewSource::Cached(full)))
122 }
123
124 fn preview_ready(&mut self, path: &Path) -> bool {
128 if self.previews.contains_key(path) {
129 return true;
130 }
131 let Some(picker) = self.picker.as_ref().map(|glue| glue.id) else {
132 return false;
133 };
134 let key = PreviewKey {
135 picker,
136 path: path.to_path_buf(),
137 };
138 if self
141 .preview_loads
142 .get(path)
143 .is_some_and(|load| load.covers(&key))
144 {
145 return false;
146 }
147 if let Some(Load::Running(old)) = self.preview_loads.get(path).cloned() {
149 if let Some(handle) = self.worker_handles.remove(&old.request) {
150 handle.cancel(CancelReason::Superseded);
151 }
152 }
153 let request = match self.worker_ids.allocate() {
154 Ok(request) => request,
155 Err(error) => {
156 self.message = error.message;
157 return false;
158 }
159 };
160 let ticket = Ticket {
161 request,
162 key: key.clone(),
163 };
164 self.preview_loads
167 .insert(path.to_path_buf(), Load::Running(ticket.clone()));
168 strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
169 serde_json::json!({
170 "service":"preview","request":request.get(),
171 "picker":picker.0.get(),"path":path.to_string_lossy(),
172 })
173 });
174 match self
175 .tape
176 .request("strop-preview", &serde_json::json!({"ticket":ticket}))
177 {
178 Ok(false) => return false,
179 Ok(true) => {}
180 Err(error) => {
181 self.handle_preview(PreviewResult {
182 ticket,
183 outcome: Outcome::failed(FailureKind::Protocol, error.to_string()),
184 });
185 return false;
186 }
187 }
188 let launch_path = path.to_path_buf();
189 let tx = self.preview_tx.clone();
190 let handle = worker::spawn(
191 "strop-preview",
192 move |outcome| {
193 let _ = tx.send(PreviewResult { ticket, outcome });
194 },
195 move |_| read_preview(&launch_path),
196 );
197 self.worker_handles.insert(request, handle);
198 false
199 }
200}
201
202pub(crate) fn read_preview(path: &Path) -> Outcome<PreparedPreview> {
206 const LIMIT: u64 = 512 * 1024;
207 let read = || -> Result<String, Failure> {
208 let meta =
209 std::fs::metadata(path).map_err(|e| Failure::new(FailureKind::Io, e.to_string()))?;
210 if !meta.is_file() {
211 return Err(Failure::new(
212 FailureKind::Unavailable,
213 "preview target is not a file",
214 ));
215 }
216 if meta.len() > LIMIT {
217 return Err(Failure::new(FailureKind::Unavailable, "preview too large"));
218 }
219 let file =
220 std::fs::File::open(path).map_err(|e| Failure::new(FailureKind::Io, e.to_string()))?;
221 let mut bytes = Vec::new();
222 file.take(LIMIT + 1)
223 .read_to_end(&mut bytes)
224 .map_err(|e| Failure::new(FailureKind::Io, e.to_string()))?;
225 if bytes.len() as u64 > LIMIT {
226 return Err(Failure::new(FailureKind::Unavailable, "preview too large"));
227 }
228 String::from_utf8(bytes).map_err(|e| Failure::new(FailureKind::Io, e.to_string()))
229 };
230 match read() {
231 Ok(text) => Outcome::Success(text.into()),
232 Err(failure) => Outcome::Failed {
233 failure,
234 partial: None,
235 },
236 }
237}