252 lines · 7.9 KB · Rust
RawDownload
use anyhow::Result;use gpui::{App, AppContext as _, Entity, Task};use language::{Anchor, BufferSnapshot, DiagnosticEntryRef, DiagnosticSeverity, ToOffset};use project::{DiagnosticSummary, Project};use rope::Point;use std::{fmt::Write, ops::RangeInclusive, path::Path};use text::OffsetRangeExt;use util::ResultExt;use util::paths::PathMatcher; pub fn codeblock_fence_for_path(    path: Option<&str>,    row_range: Option<RangeInclusive<u32>>,) -> String {    let mut text = String::new();    write!(text, "```").unwrap();     if let Some(path) = path {        if let Some(extension) = Path::new(path).extension().and_then(|ext| ext.to_str()) {            write!(text, "{} ", extension).unwrap();        }         write!(text, "{path}").unwrap();    } else {        write!(text, "untitled").unwrap();    }     if let Some(row_range) = row_range {        write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();    }     text.push('\n');    text} pub struct DiagnosticsOptions {    pub include_errors: bool,    pub include_warnings: bool,    pub path_matcher: Option<PathMatcher>,} /// Collects project diagnostics into a formatted string.////// Returns `None` if no matching diagnostics were found.pub fn collect_diagnostics(    project: Entity<Project>,    options: DiagnosticsOptions,    cx: &mut App,) -> Task<Result<Option<String>>> {    let path_style = project.read(cx).path_style(cx);    let glob_is_exact_file_match = if let Some(path) = options        .path_matcher        .as_ref()        .and_then(|pm| pm.sources().next())    {        project            .read(cx)            .find_project_path(Path::new(path), cx)            .is_some()    } else {        false    };     let project_handle = project.downgrade();    let diagnostic_summaries: Vec<_> = project        .read(cx)        .diagnostic_summaries(false, cx)        .flat_map(|(path, _, summary)| {            let worktree = project.read(cx).worktree_for_id(path.worktree_id, cx)?;            let full_path = worktree.read(cx).root_name().join(&path.path);            Some((path, full_path, summary))        })        .collect();     cx.spawn(async move |cx| {        let error_source = if let Some(path_matcher) = &options.path_matcher {            debug_assert_eq!(path_matcher.sources().count(), 1);            Some(path_matcher.sources().next().unwrap_or_default())        } else {            None        };         let mut text = String::new();        if let Some(error_source) = error_source.as_ref() {            writeln!(text, "diagnostics: {}", error_source).unwrap();        } else {            writeln!(text, "diagnostics").unwrap();        }         let mut found_any_diagnostics = false;        let mut project_summary = DiagnosticSummary::default();        for (project_path, path, summary) in diagnostic_summaries {            if let Some(path_matcher) = &options.path_matcher                && !path_matcher.is_match(&path)            {                continue;            }             let has_errors = options.include_errors && summary.error_count > 0;            let has_warnings = options.include_warnings && summary.warning_count > 0;            if !has_errors && !has_warnings {                continue;            }             if options.include_errors {                project_summary.error_count += summary.error_count;            }            if options.include_warnings {                project_summary.warning_count += summary.warning_count;            }             let file_path = path.display(path_style).to_string();            if !glob_is_exact_file_match {                writeln!(&mut text, "{file_path}").unwrap();            }             if let Some(buffer) = project_handle                .update(cx, |project, cx| project.open_buffer(project_path, cx))?                .await                .log_err()            {                let snapshot = cx.read_entity(&buffer, |buffer, _| buffer.snapshot());                if collect_buffer_diagnostics(                    &mut text,                    &snapshot,                    options.include_warnings,                    options.include_errors,                ) {                    found_any_diagnostics = true;                }            }        }         if !found_any_diagnostics {            return Ok(None);        }         let mut label = String::new();        label.push_str("Diagnostics");        if let Some(source) = error_source {            write!(label, " ({})", source).unwrap();        }         if project_summary.error_count > 0 || project_summary.warning_count > 0 {            label.push(':');             if project_summary.error_count > 0 {                write!(label, " {} errors", project_summary.error_count).unwrap();                if project_summary.warning_count > 0 {                    label.push(',');                }            }             if project_summary.warning_count > 0 {                write!(label, " {} warnings", project_summary.warning_count).unwrap();            }        }         // Prepend the summary label to the output.        text.insert_str(0, &format!("{label}\n"));         Ok(Some(text))    })} /// Collects diagnostics from a buffer snapshot into the text output.////// Returns `true` if any diagnostics were written.fn collect_buffer_diagnostics(    text: &mut String,    snapshot: &BufferSnapshot,    include_warnings: bool,    include_errors: bool,) -> bool {    let mut found_any = false;    for (_, group) in snapshot.diagnostic_groups(None) {        let entry = &group.entries[group.primary_ix];        if collect_diagnostic(text, entry, snapshot, include_warnings, include_errors) {            found_any = true;        }    }    found_any} /// Formats a single diagnostic entry as a code excerpt with the diagnostic message.////// Returns `true` if the diagnostic was written (i.e. it matched severity filters).fn collect_diagnostic(    text: &mut String,    entry: &DiagnosticEntryRef<'_, Anchor>,    snapshot: &BufferSnapshot,    include_warnings: bool,    include_errors: bool,) -> bool {    const EXCERPT_EXPANSION_SIZE: u32 = 2;    const MAX_MESSAGE_LENGTH: usize = 2000;     let ty = match entry.diagnostic.severity {        DiagnosticSeverity::WARNING => {            if !include_warnings {                return false;            }            "warning"        }        DiagnosticSeverity::ERROR => {            if !include_errors {                return false;            }            "error"        }        _ => return false,    };     let range = entry.range.to_point(snapshot);    let diagnostic_row_number = range.start.row + 1;     let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE);    let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1;    let excerpt_range =        Point::new(start_row, 0).to_offset(snapshot)..Point::new(end_row, 0).to_offset(snapshot);     text.push_str("```");    if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {        text.push_str(&language_name);    }    text.push('\n');     let mut buffer_text = String::new();    for chunk in snapshot.text_for_range(excerpt_range) {        buffer_text.push_str(chunk);    }     for (i, line) in buffer_text.lines().enumerate() {        let line_number = start_row + i as u32 + 1;        writeln!(text, "{}", line).unwrap();         if line_number == diagnostic_row_number {            text.push_str("//");            let marker_start = text.len();            write!(text, " {}: ", ty).unwrap();            let padding = text.len() - marker_start;             let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH)                .replace('\n', format!("\n//{:padding$}", "").as_str());             writeln!(text, "{message}").unwrap();        }    }     writeln!(text, "```").unwrap();    true}