1use action_log::ActionLog;
2use anyhow::{Context as _, Result};
3use gpui::{AsyncApp, Entity};
4use language::{Buffer, OutlineItem, ParseStatus};
5use project::Project;
6use regex::Regex;
7use std::fmt::Write;
8use std::path::Path;
9use text::Point;
10
11/// For files over this size, instead of reading them (or including them in context),
12/// we automatically provide the file's symbol outline instead, with line numbers.
13pub const AUTO_OUTLINE_SIZE: usize = 16384;
14
15pub async fn file_outline(
16 project: Entity<Project>,
17 path: String,
18 action_log: Entity<ActionLog>,
19 regex: Option<Regex>,
20 cx: &mut AsyncApp,
21) -> anyhow::Result<String> {
22 let buffer = {
23 let project_path = project.read_with(cx, |project, cx| {
24 project
25 .find_project_path(&path, cx)
26 .with_context(|| format!("Path {path} not found in project"))
27 })??;
28
29 project
30 .update(cx, |project, cx| project.open_buffer(project_path, cx))?
31 .await?
32 };
33
34 action_log.update(cx, |action_log, cx| {
35 action_log.buffer_read(buffer.clone(), cx);
36 })?;
37
38 // Wait until the buffer has been fully parsed, so that we can read its outline.
39 let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
40 while *parse_status.borrow() != ParseStatus::Idle {
41 parse_status.changed().await?;
42 }
43
44 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
45 let outline = snapshot.outline(None);
46
47 render_outline(
48 outline
49 .items
50 .into_iter()
51 .map(|item| item.to_point(&snapshot)),
52 regex,
53 0,
54 usize::MAX,
55 )
56 .await
57}
58
59pub async fn render_outline(
60 items: impl IntoIterator<Item = OutlineItem<Point>>,
61 regex: Option<Regex>,
62 offset: usize,
63 results_per_page: usize,
64) -> Result<String> {
65 let mut items = items.into_iter().skip(offset);
66
67 let entries = items
68 .by_ref()
69 .filter(|item| {
70 regex
71 .as_ref()
72 .is_none_or(|regex| regex.is_match(&item.text))
73 })
74 .take(results_per_page)
75 .collect::<Vec<_>>();
76 let has_more = items.next().is_some();
77
78 let mut output = String::new();
79 let entries_rendered = render_entries(&mut output, entries);
80
81 // Calculate pagination information
82 let page_start = offset + 1;
83 let page_end = offset + entries_rendered;
84 let total_symbols = if has_more {
85 format!("more than {}", page_end)
86 } else {
87 page_end.to_string()
88 };
89
90 // Add pagination information
91 if has_more {
92 writeln!(&mut output, "\nShowing symbols {page_start}-{page_end} (there were more symbols found; use offset: {page_end} to see next page)",
93 )
94 } else {
95 writeln!(
96 &mut output,
97 "\nShowing symbols {page_start}-{page_end} (total symbols: {total_symbols})",
98 )
99 }
100 .ok();
101
102 Ok(output)
103}
104
105fn render_entries(
106 output: &mut String,
107 items: impl IntoIterator<Item = OutlineItem<Point>>,
108) -> usize {
109 let mut entries_rendered = 0;
110
111 for item in items {
112 // Indent based on depth ("" for level 0, " " for level 1, etc.)
113 for _ in 0..item.depth {
114 output.push(' ');
115 }
116 output.push_str(&item.text);
117
118 // Add position information - convert to 1-based line numbers for display
119 let start_line = item.range.start.row + 1;
120 let end_line = item.range.end.row + 1;
121
122 if start_line == end_line {
123 writeln!(output, " [L{}]", start_line).ok();
124 } else {
125 writeln!(output, " [L{}-{}]", start_line, end_line).ok();
126 }
127 entries_rendered += 1;
128 }
129
130 entries_rendered
131}
132
133/// Result of getting buffer content, which can be either full content or an outline.
134pub struct BufferContent {
135 /// The actual content (either full text or outline)
136 pub text: String,
137 /// Whether this is an outline (true) or full content (false)
138 pub is_outline: bool,
139}
140
141/// Returns either the full content of a buffer or its outline, depending on size.
142/// For files larger than AUTO_OUTLINE_SIZE, returns an outline with a header.
143/// For smaller files, returns the full content.
144pub async fn get_buffer_content_or_outline(
145 buffer: Entity<Buffer>,
146 path: Option<&Path>,
147 cx: &AsyncApp,
148) -> Result<BufferContent> {
149 let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?;
150
151 if file_size > AUTO_OUTLINE_SIZE {
152 // For large files, use outline instead of full content
153 // Wait until the buffer has been fully parsed, so we can read its outline
154 let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
155 while *parse_status.borrow() != ParseStatus::Idle {
156 parse_status.changed().await?;
157 }
158
159 let outline_items = buffer.read_with(cx, |buffer, _| {
160 let snapshot = buffer.snapshot();
161 snapshot
162 .outline(None)
163 .items
164 .into_iter()
165 .map(|item| item.to_point(&snapshot))
166 .collect::<Vec<_>>()
167 })?;
168
169 let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
170
171 let text = if let Some(path) = path {
172 format!(
173 "# File outline for {} (file too large to show full content)\n\n{}",
174 path.display(),
175 outline_text
176 )
177 } else {
178 format!(
179 "# File outline (file too large to show full content)\n\n{}",
180 outline_text
181 )
182 };
183 Ok(BufferContent {
184 text,
185 is_outline: true,
186 })
187 } else {
188 // File is small enough, return full content
189 let text = buffer.read_with(cx, |buffer, _| buffer.text())?;
190 Ok(BufferContent {
191 text,
192 is_outline: false,
193 })
194 }
195}