outline.rs

  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}