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
40pub(super) struct WitnessCheck {
43 item: usize,
44 dataset: u64,
45 document: Option<(strop_core::id::DocumentId, strop_core::id::BufferRevision)>,
46 path: Option<std::path::PathBuf>,
47 range: Option<strop_core::Range>,
48}
49
50impl Editor {
51 pub fn picker_preview_range(
52 &mut self,
53 source: &PreviewSource,
54 ) -> Result<Option<strop_core::Range>, &'static str> {
55 let Some(glue) = self
56 .picker
57 .as_ref()
58 .filter(|glue| glue.picker.kind == strop_picker::Kind::Search)
59 else {
60 return Ok(None);
61 };
62 let Some(context) = glue.search.as_ref() else {
63 return Ok(None);
64 };
65 let Some(row) = glue.picker.rows.get(glue.picker.selected) else {
66 return Ok(None);
67 };
68 let (rope, document, path) = match source {
69 PreviewSource::Buffer(id) => {
70 let Some(doc) = self.docs.get(*id) else {
71 return Err("source closed — refresh Search");
72 };
73 (doc.buf.text(), Some((*id, doc.buf.revision())), None)
74 }
75 PreviewSource::Cached(path) => {
76 let Some(entry) = self.previews.get(path) else {
77 return Ok(None);
78 };
79 (&entry.rope, None, Some(path))
80 }
81 _ => return Ok(None),
82 };
83 let result = |range: Option<strop_core::Range>| {
84 range.map(Some).ok_or("source changed — refresh Search")
85 };
86 if let Some(cached) = glue.preview_witness.as_ref().filter(|cached| {
87 cached.item == row.item
88 && cached.dataset == context.stamp.dataset
89 && cached.document == document
90 && cached.path.as_ref() == path
91 }) {
92 return result(cached.range);
93 }
94 let Payload::Grep {
95 line,
96 col,
97 match_len,
98 line_text,
99 ..
100 } = &glue.picker.items[row.item].payload
101 else {
102 return Ok(None);
103 };
104 let range = super::checked_hit_range(
105 rope,
106 &super::ReplacementHit {
107 line: *line,
108 col: *col,
109 match_len: *match_len,
110 text: line_text.clone(),
111 },
112 );
113 let checked = WitnessCheck {
114 item: row.item,
115 dataset: context.stamp.dataset,
116 document,
117 path: path.cloned(),
118 range,
119 };
120 if let Some(glue) = self.picker.as_mut() {
121 glue.preview_witness = Some(checked);
122 }
123 result(range)
124 }
125
126 pub fn picker_preview(&mut self) -> Option<(String, Option<usize>, PreviewSource)> {
127 let item = self.picker.as_ref()?.picker.current()?;
128 let (path, focus_line) = match &item.payload {
129 Payload::RemoteDirectory(_)
130 | Payload::RemoteConnect
131 | Payload::Jump { .. }
132 | Payload::SearchOption(_)
133 | Payload::CodeAction(_)
134 | Payload::Container(_)
135 | Payload::IndentChoice(_) => return None,
136 Payload::Buffer(document) => {
137 let name = self.docs.get(*document)?.label(&self.cwd);
138 return Some((name, None, PreviewSource::Buffer(*document)));
139 }
140 Payload::File(path) => (path.clone(), None),
141 Payload::Grep { path, line, .. } => (path.clone(), Some(*line)),
142 Payload::Remote {
147 endpoint,
148 path,
149 line,
150 ..
151 } => {
152 return Some((
153 format!("{endpoint}{}", path.display()),
154 Some(*line),
155 PreviewSource::Failed("remote hit — accept to open".into()),
156 ));
157 }
158 };
159 let full = self.picker_path(&path);
160 let title = {
163 let name = path
164 .file_name()
165 .map(|n| n.to_string_lossy().into_owned())
166 .unwrap_or_else(|| path.display().to_string());
167 let parent = path
168 .parent()
169 .map(|p| p.display().to_string())
170 .unwrap_or_default();
171 match focus_line {
172 Some(line) => format!("{name}:{line} {parent}"),
173 None => format!("{name} {parent}"),
174 }
175 .trim_end()
176 .to_string()
177 };
178 let target = crate::files::FileTarget::Local(full.clone());
179 if let Some((document, _)) = self
180 .docs
181 .iter()
182 .find(|(_, document)| document.matches_target(&target))
183 {
184 return Some((title, focus_line, PreviewSource::Buffer(document)));
185 }
186 match self.preview_loads.get(&full) {
187 Some(Load::Failed { failure, .. }) => {
188 return Some((
189 title,
190 focus_line,
191 PreviewSource::Failed(failure.message.clone()),
192 ));
193 }
194 Some(Load::Cancelled { reason, .. }) => {
195 return Some((title, focus_line, PreviewSource::Cancelled(*reason)));
196 }
197 _ => {}
198 }
199 if !self.preview_ready(&full) {
200 return Some((title, focus_line, PreviewSource::Loading));
201 }
202 Some((title, focus_line, PreviewSource::Cached(full)))
203 }
204
205 fn preview_ready(&mut self, path: &Path) -> bool {
209 let Some(picker) = self.picker.as_ref().map(|glue| glue.id) else {
210 return false;
211 };
212 let key = PreviewKey {
213 picker,
214 path: path.to_path_buf(),
215 };
216 if self.previews.contains_key(path)
217 && matches!(self.preview_loads.get(path), Some(Load::Ready(owner)) if owner == &key)
218 {
219 return true;
220 }
221 if self
224 .preview_loads
225 .get(path)
226 .is_some_and(|load| load.covers(&key))
227 {
228 return false;
229 }
230 if let Some(Load::Running(old)) = self.preview_loads.get(path).cloned() {
232 if let Some(handle) = self.worker_handles.remove(&old.request) {
233 handle.cancel(CancelReason::Superseded);
234 }
235 }
236 let request = match self.worker_ids.allocate() {
237 Ok(request) => request,
238 Err(error) => {
239 self.message = error.message;
240 return false;
241 }
242 };
243 let ticket = Ticket {
244 request,
245 key: key.clone(),
246 };
247 self.preview_loads
250 .insert(path.to_path_buf(), Load::Running(ticket.clone()));
251 strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
252 serde_json::json!({
253 "service":"preview","request":request.get(),
254 "picker":picker.0.get(),"path":path.to_string_lossy(),
255 })
256 });
257 match self
258 .tape
259 .request("strop-preview", &serde_json::json!({"ticket":ticket}))
260 {
261 Ok(false) => return false,
262 Ok(true) => {}
263 Err(error) => {
264 self.handle_preview(PreviewResult {
265 ticket,
266 outcome: Outcome::failed(FailureKind::Protocol, error.to_string()),
267 });
268 return false;
269 }
270 }
271 let launch_path = path.to_path_buf();
272 let tx = self.preview_tx.clone();
273 let handle = worker::spawn(
274 "strop-preview",
275 move |outcome| {
276 let _ = tx.send(PreviewResult { ticket, outcome });
277 },
278 move |_| read_preview(&launch_path),
279 );
280 self.worker_handles.insert(request, handle);
281 false
282 }
283}
284
285pub(crate) fn read_preview(path: &Path) -> Outcome<PreparedPreview> {
289 const LIMIT: u64 = 512 * 1024;
290 let read = || -> Result<String, Failure> {
291 let meta =
292 std::fs::metadata(path).map_err(|e| Failure::new(FailureKind::Io, e.to_string()))?;
293 if !meta.is_file() {
294 return Err(Failure::new(
295 FailureKind::Unavailable,
296 "preview target is not a file",
297 ));
298 }
299 if meta.len() > LIMIT {
300 return Err(Failure::new(FailureKind::Unavailable, "preview too large"));
301 }
302 let file =
303 std::fs::File::open(path).map_err(|e| Failure::new(FailureKind::Io, e.to_string()))?;
304 let mut bytes = Vec::new();
305 file.take(LIMIT + 1)
306 .read_to_end(&mut bytes)
307 .map_err(|e| Failure::new(FailureKind::Io, e.to_string()))?;
308 if bytes.len() as u64 > LIMIT {
309 return Err(Failure::new(FailureKind::Unavailable, "preview too large"));
310 }
311 String::from_utf8(bytes).map_err(|e| Failure::new(FailureKind::Io, e.to_string()))
312 };
313 match read() {
314 Ok(text) => Outcome::Success(text.into()),
315 Err(failure) => Outcome::Failed {
316 failure,
317 partial: None,
318 },
319 }
320}