1use anyhow::Result;
  2use gpui::{AsyncApp, Entity};
  3use language::{Buffer, OutlineItem, ParseStatus};
  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        let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
 34        while *parse_status.borrow() != ParseStatus::Idle {
 35            parse_status.changed().await?;
 36        }
 37
 38        let outline_items = buffer.read_with(cx, |buffer, _| {
 39            let snapshot = buffer.snapshot();
 40            snapshot
 41                .outline(None)
 42                .items
 43                .into_iter()
 44                .map(|item| item.to_point(&snapshot))
 45                .collect::<Vec<_>>()
 46        })?;
 47
 48        let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
 49
 50        let text = if let Some(path) = path {
 51            format!(
 52                "# File outline for {path} (file too large to show full content)\n\n{outline_text}",
 53            )
 54        } else {
 55            format!("# File outline (file too large to show full content)\n\n{outline_text}",)
 56        };
 57        Ok(BufferContent {
 58            text,
 59            is_outline: true,
 60        })
 61    } else {
 62        // File is small enough, return full content
 63        let text = buffer.read_with(cx, |buffer, _| buffer.text())?;
 64        Ok(BufferContent {
 65            text,
 66            is_outline: false,
 67        })
 68    }
 69}
 70
 71async fn render_outline(
 72    items: impl IntoIterator<Item = OutlineItem<Point>>,
 73    regex: Option<Regex>,
 74    offset: usize,
 75    results_per_page: usize,
 76) -> Result<String> {
 77    let mut items = items.into_iter().skip(offset);
 78
 79    let entries = items
 80        .by_ref()
 81        .filter(|item| {
 82            regex
 83                .as_ref()
 84                .is_none_or(|regex| regex.is_match(&item.text))
 85        })
 86        .take(results_per_page)
 87        .collect::<Vec<_>>();
 88    let has_more = items.next().is_some();
 89
 90    let mut output = String::new();
 91    let entries_rendered = render_entries(&mut output, entries);
 92
 93    // Calculate pagination information
 94    let page_start = offset + 1;
 95    let page_end = offset + entries_rendered;
 96    let total_symbols = if has_more {
 97        format!("more than {}", page_end)
 98    } else {
 99        page_end.to_string()
100    };
101
102    // Add pagination information
103    if has_more {
104        writeln!(&mut output, "\nShowing symbols {page_start}-{page_end} (there were more symbols found; use offset: {page_end} to see next page)",
105        )
106    } else {
107        writeln!(
108            &mut output,
109            "\nShowing symbols {page_start}-{page_end} (total symbols: {total_symbols})",
110        )
111    }
112    .ok();
113
114    Ok(output)
115}
116
117fn render_entries(
118    output: &mut String,
119    items: impl IntoIterator<Item = OutlineItem<Point>>,
120) -> usize {
121    let mut entries_rendered = 0;
122
123    for item in items {
124        // Indent based on depth ("" for level 0, "  " for level 1, etc.)
125        for _ in 0..item.depth {
126            output.push(' ');
127        }
128        output.push_str(&item.text);
129
130        // Add position information - convert to 1-based line numbers for display
131        let start_line = item.range.start.row + 1;
132        let end_line = item.range.end.row + 1;
133
134        if start_line == end_line {
135            writeln!(output, " [L{}]", start_line).ok();
136        } else {
137            writeln!(output, " [L{}-{}]", start_line, end_line).ok();
138        }
139        entries_rendered += 1;
140    }
141
142    entries_rendered
143}