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