1use anyhow::Result;
2use gpui::{AsyncApp, Entity};
3use language::{Buffer, OutlineItem};
4use regex::Regex;
5use std::fmt::Write;
6use text::Point;
7
8/// For files over this size, instead of reading them (or including them in context),
9/// we automatically provide the file's symbol outline instead, with line numbers.
10pub const AUTO_OUTLINE_SIZE: usize = 16384;
11
12/// Result of getting buffer content, which can be either full content or an outline.
13pub struct BufferContent {
14 /// The actual content (either full text or outline)
15 pub text: String,
16 /// Whether this is an outline (true) or full content (false)
17 pub is_outline: bool,
18}
19
20/// Returns either the full content of a buffer or its outline, depending on size.
21/// For files larger than AUTO_OUTLINE_SIZE, returns an outline with a header.
22/// For smaller files, returns the full content.
23pub async fn get_buffer_content_or_outline(
24 buffer: Entity<Buffer>,
25 path: Option<&str>,
26 cx: &AsyncApp,
27) -> Result<BufferContent> {
28 let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?;
29
30 if file_size > AUTO_OUTLINE_SIZE {
31 // For large files, use outline instead of full content
32 // Wait until the buffer has been fully parsed, so we can read its outline
33 buffer
34 .read_with(cx, |buffer, _| buffer.parsing_idle())?
35 .await;
36
37 let outline_items = buffer.read_with(cx, |buffer, _| {
38 let snapshot = buffer.snapshot();
39 snapshot
40 .outline(None)
41 .items
42 .into_iter()
43 .map(|item| item.to_point(&snapshot))
44 .collect::<Vec<_>>()
45 })?;
46
47 // If no outline exists, fall back to first 1KB so the agent has some context
48 if outline_items.is_empty() {
49 let text = buffer.read_with(cx, |buffer, _| {
50 let snapshot = buffer.snapshot();
51 let len = snapshot.len().min(snapshot.as_rope().floor_char_boundary(1024));
52 let content = snapshot.text_for_range(0..len).collect::<String>();
53 if let Some(path) = path {
54 format!("# First 1KB of {path} (file too large to show full content, and no outline available)\n\n{content}")
55 } else {
56 format!("# First 1KB of file (file too large to show full content, and no outline available)\n\n{content}")
57 }
58 })?;
59
60 return Ok(BufferContent {
61 text,
62 is_outline: false,
63 });
64 }
65
66 let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
67
68 let text = if let Some(path) = path {
69 format!("# File outline for {path}\n\n{outline_text}",)
70 } else {
71 format!("# File outline\n\n{outline_text}",)
72 };
73 Ok(BufferContent {
74 text,
75 is_outline: true,
76 })
77 } else {
78 // File is small enough, return full content
79 let text = buffer.read_with(cx, |buffer, _| buffer.text())?;
80 Ok(BufferContent {
81 text,
82 is_outline: false,
83 })
84 }
85}
86
87async fn render_outline(
88 items: impl IntoIterator<Item = OutlineItem<Point>>,
89 regex: Option<Regex>,
90 offset: usize,
91 results_per_page: usize,
92) -> Result<String> {
93 let mut items = items.into_iter().skip(offset);
94
95 let entries = items
96 .by_ref()
97 .filter(|item| {
98 regex
99 .as_ref()
100 .is_none_or(|regex| regex.is_match(&item.text))
101 })
102 .take(results_per_page)
103 .collect::<Vec<_>>();
104 let has_more = items.next().is_some();
105
106 let mut output = String::new();
107 let entries_rendered = render_entries(&mut output, entries);
108
109 // Calculate pagination information
110 let page_start = offset + 1;
111 let page_end = offset + entries_rendered;
112 let total_symbols = if has_more {
113 format!("more than {}", page_end)
114 } else {
115 page_end.to_string()
116 };
117
118 // Add pagination information
119 if has_more {
120 writeln!(&mut output, "\nShowing symbols {page_start}-{page_end} (there were more symbols found; use offset: {page_end} to see next page)",
121 )
122 } else {
123 writeln!(
124 &mut output,
125 "\nShowing symbols {page_start}-{page_end} (total symbols: {total_symbols})",
126 )
127 }
128 .ok();
129
130 Ok(output)
131}
132
133fn render_entries(
134 output: &mut String,
135 items: impl IntoIterator<Item = OutlineItem<Point>>,
136) -> usize {
137 let mut entries_rendered = 0;
138
139 for item in items {
140 // Indent based on depth ("" for level 0, " " for level 1, etc.)
141 for _ in 0..item.depth {
142 output.push(' ');
143 }
144 output.push_str(&item.text);
145
146 // Add position information - convert to 1-based line numbers for display
147 let start_line = item.range.start.row + 1;
148 let end_line = item.range.end.row + 1;
149
150 if start_line == end_line {
151 writeln!(output, " [L{}]", start_line).ok();
152 } else {
153 writeln!(output, " [L{}-{}]", start_line, end_line).ok();
154 }
155 entries_rendered += 1;
156 }
157
158 entries_rendered
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use fs::FakeFs;
165 use gpui::TestAppContext;
166 use project::Project;
167 use settings::SettingsStore;
168
169 #[gpui::test]
170 async fn test_large_file_fallback_to_subset(cx: &mut TestAppContext) {
171 cx.update(|cx| {
172 let settings = SettingsStore::test(cx);
173 cx.set_global(settings);
174 });
175
176 let fs = FakeFs::new(cx.executor());
177 let project = Project::test(fs, [], cx).await;
178
179 let content = "⚡".repeat(100 * 1024); // 100KB
180 let content_len = content.len();
181 let buffer = project
182 .update(cx, |project, cx| project.create_buffer(true, cx))
183 .await
184 .expect("failed to create buffer");
185
186 buffer.update(cx, |buffer, cx| buffer.set_text(content, cx));
187
188 let result = cx
189 .spawn(|cx| async move { get_buffer_content_or_outline(buffer, None, &cx).await })
190 .await
191 .unwrap();
192
193 // Should contain some of the actual file content
194 assert!(
195 result.text.contains("⚡⚡⚡⚡⚡⚡⚡"),
196 "Result did not contain content subset"
197 );
198
199 // Should be marked as not an outline (it's truncated content)
200 assert!(
201 !result.is_outline,
202 "Large file without outline should not be marked as outline"
203 );
204
205 // Should be reasonably sized (much smaller than original)
206 assert!(
207 result.text.len() < 50 * 1024,
208 "Result size {} should be smaller than 50KB",
209 result.text.len()
210 );
211
212 // Should be significantly smaller than the original content
213 assert!(
214 result.text.len() < content_len / 10,
215 "Result should be much smaller than original content"
216 );
217 }
218}