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 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}