1use crate::RelatedExcerpt;
2use language::{BufferSnapshot, OffsetRangeExt as _, Point};
3use std::ops::Range;
4
5#[cfg(not(test))]
6const MAX_OUTLINE_ITEM_BODY_SIZE: usize = 512;
7#[cfg(test)]
8const MAX_OUTLINE_ITEM_BODY_SIZE: usize = 24;
9
10pub fn assemble_excerpts(
11 buffer: &BufferSnapshot,
12 mut input_ranges: Vec<Range<Point>>,
13) -> Vec<RelatedExcerpt> {
14 merge_ranges(&mut input_ranges);
15
16 let mut outline_ranges = Vec::new();
17 let outline_items = buffer.outline_items_as_points_containing(0..buffer.len(), false, None);
18 let mut outline_ix = 0;
19 for input_range in &mut input_ranges {
20 *input_range = clip_range_to_lines(input_range, false, buffer);
21
22 while let Some(outline_item) = outline_items.get(outline_ix) {
23 let item_range = clip_range_to_lines(&outline_item.range, false, buffer);
24
25 if item_range.start > input_range.start {
26 break;
27 }
28
29 if item_range.end > input_range.start {
30 let body_range = outline_item
31 .body_range(buffer)
32 .map(|body| clip_range_to_lines(&body, true, buffer))
33 .filter(|body_range| {
34 body_range.to_offset(buffer).len() > MAX_OUTLINE_ITEM_BODY_SIZE
35 });
36
37 add_outline_item(
38 item_range.clone(),
39 body_range.clone(),
40 buffer,
41 &mut outline_ranges,
42 );
43
44 if let Some(body_range) = body_range
45 && input_range.start < body_range.start
46 {
47 let mut child_outline_ix = outline_ix + 1;
48 while let Some(next_outline_item) = outline_items.get(child_outline_ix) {
49 if next_outline_item.range.end > body_range.end {
50 break;
51 }
52 if next_outline_item.depth == outline_item.depth + 1 {
53 let next_item_range =
54 clip_range_to_lines(&next_outline_item.range, false, buffer);
55
56 add_outline_item(
57 next_item_range,
58 next_outline_item
59 .body_range(buffer)
60 .map(|body| clip_range_to_lines(&body, true, buffer)),
61 buffer,
62 &mut outline_ranges,
63 );
64 child_outline_ix += 1;
65 }
66 }
67 }
68 }
69
70 outline_ix += 1;
71 }
72 }
73
74 input_ranges.extend_from_slice(&outline_ranges);
75 merge_ranges(&mut input_ranges);
76
77 input_ranges
78 .into_iter()
79 .map(|range| {
80 let offset_range = range.to_offset(buffer);
81 RelatedExcerpt {
82 point_range: range,
83 anchor_range: buffer.anchor_before(offset_range.start)
84 ..buffer.anchor_after(offset_range.end),
85 text: buffer.as_rope().slice(offset_range),
86 }
87 })
88 .collect()
89}
90
91fn clip_range_to_lines(
92 range: &Range<Point>,
93 inward: bool,
94 buffer: &BufferSnapshot,
95) -> Range<Point> {
96 let mut range = range.clone();
97 if inward {
98 if range.start.column > 0 {
99 range.start.column = buffer.line_len(range.start.row);
100 }
101 range.end.column = 0;
102 } else {
103 range.start.column = 0;
104 if range.end.column > 0 {
105 range.end.column = buffer.line_len(range.end.row);
106 }
107 }
108 range
109}
110
111fn add_outline_item(
112 mut item_range: Range<Point>,
113 body_range: Option<Range<Point>>,
114 buffer: &BufferSnapshot,
115 outline_ranges: &mut Vec<Range<Point>>,
116) {
117 if let Some(mut body_range) = body_range {
118 if body_range.start.column > 0 {
119 body_range.start.column = buffer.line_len(body_range.start.row);
120 }
121 body_range.end.column = 0;
122
123 let head_range = item_range.start..body_range.start;
124 if head_range.start < head_range.end {
125 outline_ranges.push(head_range);
126 }
127
128 let tail_range = body_range.end..item_range.end;
129 if tail_range.start < tail_range.end {
130 outline_ranges.push(tail_range);
131 }
132 } else {
133 item_range.start.column = 0;
134 item_range.end.column = buffer.line_len(item_range.end.row);
135 outline_ranges.push(item_range);
136 }
137}
138
139pub fn merge_ranges(ranges: &mut Vec<Range<Point>>) {
140 ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
141
142 let mut index = 1;
143 while index < ranges.len() {
144 let mut prev_range_end = ranges[index - 1].end;
145 if prev_range_end.column > 0 {
146 prev_range_end += Point::new(1, 0);
147 }
148
149 if (prev_range_end + Point::new(1, 0))
150 .cmp(&ranges[index].start)
151 .is_ge()
152 {
153 let removed = ranges.remove(index);
154 if removed.end.cmp(&ranges[index - 1].end).is_gt() {
155 ranges[index - 1].end = removed.end;
156 }
157 } else {
158 index += 1;
159 }
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use gpui::{TestAppContext, prelude::*};
167 use indoc::indoc;
168 use language::{Buffer, Language, LanguageConfig, LanguageMatcher, OffsetRangeExt};
169 use pretty_assertions::assert_eq;
170 use std::{fmt::Write as _, sync::Arc};
171 use util::test::marked_text_ranges;
172
173 #[gpui::test]
174 fn test_rust(cx: &mut TestAppContext) {
175 let table = [
176 (
177 indoc! {r#"
178 struct User {
179 first_name: String,
180 «last_name»: String,
181 age: u32,
182 email: String,
183 create_at: Instant,
184 }
185
186 impl User {
187 pub fn first_name(&self) -> String {
188 self.first_name.clone()
189 }
190
191 pub fn full_name(&self) -> String {
192 « format!("{} {}", self.first_name, self.last_name)
193 » }
194 }
195 "#},
196 indoc! {r#"
197 struct User {
198 first_name: String,
199 last_name: String,
200 …
201 }
202
203 impl User {
204 …
205 pub fn full_name(&self) -> String {
206 format!("{} {}", self.first_name, self.last_name)
207 }
208 }
209 "#},
210 ),
211 (
212 indoc! {r#"
213 struct «User» {
214 first_name: String,
215 last_name: String,
216 age: u32,
217 }
218
219 impl User {
220 // methods
221 }
222 "#
223 },
224 indoc! {r#"
225 struct User {
226 first_name: String,
227 last_name: String,
228 age: u32,
229 }
230 …
231 "#},
232 ),
233 (
234 indoc! {r#"
235 trait «FooProvider» {
236 const NAME: &'static str;
237
238 fn provide_foo(&self, id: usize) -> Foo;
239
240 fn provide_foo_batched(&self, ids: &[usize]) -> Vec<Foo> {
241 ids.iter()
242 .map(|id| self.provide_foo(*id))
243 .collect()
244 }
245
246 fn sync(&self);
247 }
248 "#
249 },
250 indoc! {r#"
251 trait FooProvider {
252 const NAME: &'static str;
253
254 fn provide_foo(&self, id: usize) -> Foo;
255
256 fn provide_foo_batched(&self, ids: &[usize]) -> Vec<Foo> {
257 …
258 }
259
260 fn sync(&self);
261 }
262 "#},
263 ),
264 ];
265
266 for (input, expected_output) in table {
267 let (input, ranges) = marked_text_ranges(&input, false);
268 let buffer =
269 cx.new(|cx| Buffer::local(input, cx).with_language(Arc::new(rust_lang()), cx));
270 buffer.read_with(cx, |buffer, _cx| {
271 let ranges: Vec<Range<Point>> = ranges
272 .into_iter()
273 .map(|range| range.to_point(&buffer))
274 .collect();
275
276 let excerpts = assemble_excerpts(&buffer.snapshot(), ranges);
277
278 let output = format_excerpts(buffer, &excerpts);
279 assert_eq!(output, expected_output);
280 });
281 }
282 }
283
284 fn format_excerpts(buffer: &Buffer, excerpts: &[RelatedExcerpt]) -> String {
285 let mut output = String::new();
286 let file_line_count = buffer.max_point().row;
287 let mut current_row = 0;
288 for excerpt in excerpts {
289 if excerpt.text.is_empty() {
290 continue;
291 }
292 if current_row < excerpt.point_range.start.row {
293 writeln!(&mut output, "…").unwrap();
294 }
295 current_row = excerpt.point_range.start.row;
296
297 for line in excerpt.text.to_string().lines() {
298 output.push_str(line);
299 output.push('\n');
300 current_row += 1;
301 }
302 }
303 if current_row < file_line_count {
304 writeln!(&mut output, "…").unwrap();
305 }
306 output
307 }
308
309 fn rust_lang() -> Language {
310 Language::new(
311 LanguageConfig {
312 name: "Rust".into(),
313 matcher: LanguageMatcher {
314 path_suffixes: vec!["rs".to_string()],
315 ..Default::default()
316 },
317 ..Default::default()
318 },
319 Some(language::tree_sitter_rust::LANGUAGE.into()),
320 )
321 .with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
322 .unwrap()
323 }
324}