outline.rs

  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}