strop_engine/editor/collections/
projection.rs1use super::{Collection, CollectionRow, CollectionRowInfo, Document, Editor};
3use strop_core::id::{Arena, DocumentId, DocumentKind};
4use strop_core::Range;
5
6pub(super) fn render(
13 docs: &Arena<DocumentKind, Document>,
14 cwd: &std::path::Path,
15 collection: &mut Collection,
16) -> String {
17 let files = collection
18 .excerpts
19 .iter()
20 .enumerate()
21 .filter(|(index, excerpt)| {
22 *index == 0 || collection.excerpts[index - 1].source != excerpt.source
23 })
24 .count();
25 let unavailable = if collection.skipped == 0 {
26 String::new()
27 } else {
28 format!(" · {} unavailable hit(s)", collection.skipped)
29 };
30 let mut text = format!(
31 "collection: {} — {} match(es) · {} file(s){unavailable}\n",
32 collection.title, collection.match_count, files
33 );
34 let mut rows = vec![CollectionRow::Title];
35 let mut line = 1;
36 let mut at = 0;
37 while at < collection.excerpts.len() {
38 let source = collection.excerpts[at].source;
39 let mut card_end = at;
40 while card_end + 1 < collection.excerpts.len()
41 && collection.excerpts[card_end + 1].source == source
42 {
43 card_end += 1;
44 }
45 let doc = docs.get(source).unwrap();
46 let path = doc.label(cwd);
47 let lang = doc
48 .syntax_path()
49 .and_then(|path| path.extension())
50 .and_then(|extension| extension.to_str())
51 .and_then(strop_lsp::registry::language_for_extension_name);
52 let count = card_end - at + 1;
53 let lang_badge = lang.map(|l| format!("{l} · ")).unwrap_or_default();
54 let context = collection.excerpts[at..=card_end]
55 .iter()
56 .map(|excerpt| excerpt.context)
57 .max()
58 .unwrap_or(0);
59 text.push_str(&format!(
60 "╭─ {path} ── {lang_badge}{count} excerpt(s) · context {context} (+/-)\n"
61 ));
62 rows.push(CollectionRow::CardTop(at));
63 line += 1;
64 for i in at..=card_end {
65 if i > at {
66 let prev = &collection.excerpts[i - 1];
67 let here = &collection.excerpts[i];
68 let gap_lines = doc
69 .buf
70 .line_of(here.start)
71 .saturating_sub(doc.buf.line_of(prev.end));
72 text.push_str(&format!("⋮ {gap_lines} source lines omitted\n"));
73 rows.push(CollectionRow::Gap);
74 line += 1;
75 }
76 let start = collection.excerpts[i].start;
77 let end = collection.excerpts[i].end;
78 let body = doc.buf.text().byte_slice(start..end).to_string();
79 let excerpt = &mut collection.excerpts[i];
80 excerpt.view_line = line - 1; excerpt.view_start = text.len();
82 excerpt.view_lines = body.lines().count().max(1);
83 text.push_str(&body);
84 if !body.ends_with('\n') {
85 text.push('\n');
86 }
87 excerpt.view_end = text.len();
88 for _ in 0..excerpt.view_lines {
89 rows.push(CollectionRow::Body);
90 }
91 line += excerpt.view_lines;
92 }
93 text.push_str("╰\n");
94 rows.push(CollectionRow::CardBottom);
95 line += 1;
96 at = card_end + 1;
97 }
98 collection.rows = rows;
99 text
100}
101
102impl Editor {
103 pub(crate) fn collection_render_view(&mut self, id: DocumentId) {
106 let caret = if self.current() == id {
109 Some((
110 self.buf().line_of(self.head()),
111 self.buf().col_of(self.head()),
112 ))
113 } else {
114 None
115 };
116 let text = {
117 let entry = self.collections.get_mut(&id).unwrap();
118 render(&self.docs, &self.cwd, entry)
119 };
120 let _ = self.doc_mut(id).buf.system_edit().replace_all(&text);
121 if let Some((line, col)) = caret {
122 let line = line.min(self.docs.get(id).unwrap().buf.len_lines().saturating_sub(1));
123 let head = self.docs.get(id).unwrap().buf.clamp_boundary(
124 self.docs
125 .get(id)
126 .unwrap()
127 .buf
128 .line_start(line)
129 .saturating_add(col),
130 );
131 if self.current() == id {
132 self.set_head(head);
133 self.clamp_cursor();
134 }
135 }
136 let revision = self.docs.get(id).unwrap().buf.revision();
137 self.collections.get_mut(&id).unwrap().revision = revision;
138 self.docs.get_mut(id).unwrap().buf.dirty = false;
141 }
142
143 pub(crate) fn collection_splice_excerpt(&mut self, id: DocumentId, index: usize) {
148 let (source, view_start, view_end, view_lines) = {
149 let entry = &self.collections[&id];
150 let excerpt = &entry.excerpts[index];
151 (
152 excerpt.source,
153 excerpt.view_start,
154 excerpt.view_end,
155 excerpt.view_lines,
156 )
157 };
158 let Some(source_doc) = self.docs.get(source) else {
159 return;
160 };
161 let excerpt_span = {
162 let entry = &self.collections[&id];
163 let excerpt = &entry.excerpts[index];
164 (excerpt.start, excerpt.end)
165 };
166 let mut body = source_doc
167 .buf
168 .text()
169 .byte_slice(excerpt_span.0..excerpt_span.1)
170 .to_string();
171 if !body.ends_with('\n') {
172 body.push('\n');
173 }
174 let new_lines = body.lines().count().max(1);
175 {
178 let doc = self.docs.get_mut(id).unwrap();
179 let _ = doc
180 .buf
181 .system_edit()
182 .replace(Range::charwise(view_start, view_end), &body);
183 }
184 {
187 let doc = self.docs.get_mut(id).unwrap();
188 let changes: Vec<_> = doc.buf.changes().to_vec();
189 self.analysis.edits(id, &changes);
190 doc.buf.clear_changes();
191 }
192 let byte_delta = body.len() as isize - (view_end - view_start) as isize;
193 let line_delta = new_lines as isize - view_lines as isize;
194 let entry = self.collections.get_mut(&id).unwrap();
195 let first_row = entry.excerpts[index].view_line + 1;
196 entry.rows.splice(
197 first_row..first_row + view_lines,
198 std::iter::repeat_n(CollectionRow::Body, new_lines),
199 );
200 let mut seen = false;
201 for excerpt in &mut entry.excerpts {
202 if seen {
203 excerpt.view_start = (excerpt.view_start as isize + byte_delta) as usize;
204 excerpt.view_end = (excerpt.view_end as isize + byte_delta) as usize;
205 excerpt.view_line = (excerpt.view_line as isize + line_delta) as usize;
206 } else if excerpt.view_start == view_start {
207 seen = true;
208 excerpt.view_lines = new_lines;
209 excerpt.view_end = view_start + body.len();
210 }
211 }
212 let revision = self.docs.get(id).unwrap().buf.revision();
213 self.collections.get_mut(&id).unwrap().revision = revision;
214 }
215}
216
217impl Editor {
218 pub fn collection_row_info(
223 &self,
224 doc: strop_core::id::DocumentId,
225 line: usize,
226 ) -> Option<CollectionRowInfo> {
227 let collection = self.collections.get(&doc)?;
228 let kind = *collection.rows.get(line)?;
229 let caret_line = if doc == self.current() {
230 self.buf().line_of(self.head())
231 } else {
232 usize::MAX
233 };
234 let mut info = CollectionRowInfo {
235 kind,
236 source: None,
237 source_matches: Vec::new(),
238 card_active: false,
239 source_dirty: false,
240 source_readonly: false,
241 };
242 let index = collection
243 .excerpts
244 .partition_point(|excerpt| excerpt.view_line <= line);
245 if let Some(excerpt) = index
246 .checked_sub(1)
247 .and_then(|index| collection.excerpts.get(index))
248 {
249 if let Some(source) = self.docs.get(excerpt.source) {
250 info.source_dirty = source.buf.dirty;
251 info.source_readonly = source.buf.readonly;
252 }
253 let active_index = collection
254 .excerpts
255 .partition_point(|excerpt| excerpt.view_line <= caret_line);
256 info.card_active = caret_line != usize::MAX
257 && active_index
258 .checked_sub(1)
259 .and_then(|index| collection.excerpts.get(index))
260 .is_some_and(|active| active.source == excerpt.source);
261 if kind == CollectionRow::Body
262 && line > excerpt.view_line
263 && line <= excerpt.view_line + excerpt.view_lines
264 {
265 let source = self.docs.get(excerpt.source)?;
266 let source_line = source.buf.line_of(excerpt.start) + line - excerpt.view_line - 1;
267 let start = source.buf.line_start(source_line);
268 let end = source.buf.line_end(source_line);
269 info.source = Some((excerpt.source, start, end));
270 let first = excerpt
271 .matches
272 .partition_point(|(offset, length)| offset.saturating_add(*length) <= start);
273 info.source_matches = excerpt.matches[first..]
274 .iter()
275 .copied()
276 .take_while(|(offset, _)| *offset < end)
277 .map(|(offset, length)| {
278 (
279 offset.max(start),
280 offset.saturating_add(length).min(end) - offset.max(start),
281 )
282 })
283 .collect();
284 }
285 }
286 Some(info)
287 }
288
289 pub fn collection_row_kind(
292 &self,
293 doc: strop_core::id::DocumentId,
294 line: usize,
295 ) -> Option<CollectionRow> {
296 self.collections.get(&doc)?.rows.get(line).copied()
297 }
298
299 pub fn collection_source_lineno(
304 &self,
305 doc: strop_core::id::DocumentId,
306 line: usize,
307 ) -> Option<Option<usize>> {
308 let collection = self.collections.get(&doc)?;
309 let index = collection
310 .excerpts
311 .partition_point(|excerpt| excerpt.view_line < line);
312 if let Some(excerpt) = index
313 .checked_sub(1)
314 .and_then(|index| collection.excerpts.get(index))
315 {
316 if line <= excerpt.view_line + excerpt.view_lines {
317 let source = self.docs.get(excerpt.source)?;
318 return Some(Some(
319 source.buf.line_of(excerpt.start) + line - excerpt.view_line,
320 ));
321 }
322 }
323 Some(None) }
325}
326
327impl Editor {
328 pub fn collection_unsaved(&self, document: DocumentId) -> bool {
329 self.collections.get(&document).is_some_and(|collection| {
330 collection.excerpts.iter().any(|excerpt| {
331 self.docs
332 .get(excerpt.source)
333 .is_some_and(|source| source.buf.dirty)
334 })
335 })
336 }
337}
338
339impl Editor {
340 pub fn source_position(
342 &self,
343 document: DocumentId,
344 byte: usize,
345 ) -> Option<(DocumentId, usize)> {
346 let view = self.docs.get(document)?;
347 let Some(collection) = self.collections.get(&document) else {
348 return Some((
349 document,
350 view.buf.clamp_boundary(byte.min(view.buf.len_bytes())),
351 ));
352 };
353 let after = collection
354 .excerpts
355 .partition_point(|excerpt| excerpt.view_start <= byte);
356 let excerpt = collection.excerpts.get(after.checked_sub(1)?)?;
357 if byte >= excerpt.view_end {
358 return None;
359 }
360 let source = self.docs.get(excerpt.source)?;
361 let offset = (excerpt.start + byte - excerpt.view_start).min(excerpt.end);
362 Some((
363 excerpt.source,
364 source
365 .buf
366 .clamp_boundary(offset.min(source.buf.len_bytes())),
367 ))
368 }
369}