1use action_log::ActionLog;
2use anyhow::{Context as _, Result};
3use gpui::{AsyncApp, Entity};
4use language::{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
45 .outline(None)
46 .context("No outline information available for this file at path {path}")?;
47
48 render_outline(
49 outline
50 .items
51 .into_iter()
52 .map(|item| item.to_point(&snapshot)),
53 regex,
54 0,
55 usize::MAX,
56 )
57 .await
58}
59
60pub async fn render_outline(
61 items: impl IntoIterator<Item = OutlineItem<Point>>,
62 regex: Option<Regex>,
63 offset: usize,
64 results_per_page: usize,
65) -> Result<String> {
66 let mut items = items.into_iter().skip(offset);
67
68 let entries = items
69 .by_ref()
70 .filter(|item| {
71 regex
72 .as_ref()
73 .is_none_or(|regex| regex.is_match(&item.text))
74 })
75 .take(results_per_page)
76 .collect::<Vec<_>>();
77 let has_more = items.next().is_some();
78
79 let mut output = String::new();
80 let entries_rendered = render_entries(&mut output, entries);
81
82 // Calculate pagination information
83 let page_start = offset + 1;
84 let page_end = offset + entries_rendered;
85 let total_symbols = if has_more {
86 format!("more than {}", page_end)
87 } else {
88 page_end.to_string()
89 };
90
91 // Add pagination information
92 if has_more {
93 writeln!(&mut output, "\nShowing symbols {page_start}-{page_end} (there were more symbols found; use offset: {page_end} to see next page)",
94 )
95 } else {
96 writeln!(
97 &mut output,
98 "\nShowing symbols {page_start}-{page_end} (total symbols: {total_symbols})",
99 )
100 }
101 .ok();
102
103 Ok(output)
104}
105
106fn render_entries(
107 output: &mut String,
108 items: impl IntoIterator<Item = OutlineItem<Point>>,
109) -> usize {
110 let mut entries_rendered = 0;
111
112 for item in items {
113 // Indent based on depth ("" for level 0, " " for level 1, etc.)
114 for _ in 0..item.depth {
115 output.push(' ');
116 }
117 output.push_str(&item.text);
118
119 // Add position information - convert to 1-based line numbers for display
120 let start_line = item.range.start.row + 1;
121 let end_line = item.range.end.row + 1;
122
123 if start_line == end_line {
124 writeln!(output, " [L{}]", start_line).ok();
125 } else {
126 writeln!(output, " [L{}-{}]", start_line, end_line).ok();
127 }
128 entries_rendered += 1;
129 }
130
131 entries_rendered
132}