451 lines · 14.4 KB · Rust
RawDownload
use anyhow::{Context as _, Result, anyhow};use assistant_slash_command::{    ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,    SlashCommandResult,};use fuzzy::{PathMatch, StringMatchCandidate};use gpui::{App, Entity, Task, WeakEntity};use language::{    Anchor, BufferSnapshot, DiagnosticEntryRef, DiagnosticSeverity, LspAdapterDelegate,    OffsetRangeExt, ToOffset,};use project::{DiagnosticSummary, PathMatchCandidateSet, Project};use rope::Point;use std::{    fmt::Write,    path::Path,    sync::{Arc, atomic::AtomicBool},};use ui::prelude::*;use util::paths::{PathMatcher, PathStyle};use util::{ResultExt, rel_path::RelPath};use workspace::Workspace; use crate::create_label_for_command; pub struct DiagnosticsSlashCommand; impl DiagnosticsSlashCommand {    fn search_paths(        &self,        query: String,        cancellation_flag: Arc<AtomicBool>,        workspace: &Entity<Workspace>,        cx: &mut App,    ) -> Task<Vec<PathMatch>> {        if query.is_empty() {            let workspace = workspace.read(cx);            let entries = workspace.recent_navigation_history(Some(10), cx);            let path_prefix: Arc<RelPath> = RelPath::empty().into();            Task::ready(                entries                    .into_iter()                    .map(|(entry, _)| PathMatch {                        score: 0.,                        positions: Vec::new(),                        worktree_id: entry.worktree_id.to_usize(),                        path: entry.path,                        path_prefix: path_prefix.clone(),                        is_dir: false, // Diagnostics can't be produced for directories                        distance_to_relative_ancestor: 0,                    })                    .collect(),            )        } else {            let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();            let candidate_sets = worktrees                .into_iter()                .map(|worktree| {                    let worktree = worktree.read(cx);                    PathMatchCandidateSet {                        snapshot: worktree.snapshot(),                        include_ignored: worktree                            .root_entry()                            .is_some_and(|entry| entry.is_ignored),                        include_root_name: true,                        candidates: project::Candidates::Entries,                    }                })                .collect::<Vec<_>>();             let executor = cx.background_executor().clone();            cx.foreground_executor().spawn(async move {                fuzzy::match_path_sets(                    candidate_sets.as_slice(),                    query.as_str(),                    &None,                    false,                    100,                    &cancellation_flag,                    executor,                )                .await            })        }    }} impl SlashCommand for DiagnosticsSlashCommand {    fn name(&self) -> String {        "diagnostics".into()    }     fn label(&self, cx: &App) -> language::CodeLabel {        create_label_for_command("diagnostics", &[INCLUDE_WARNINGS_ARGUMENT], cx)    }     fn description(&self) -> String {        "Insert diagnostics".into()    }     fn icon(&self) -> IconName {        IconName::XCircle    }     fn menu_text(&self) -> String {        self.description()    }     fn requires_argument(&self) -> bool {        false    }     fn accepts_arguments(&self) -> bool {        true    }     fn complete_argument(        self: Arc<Self>,        arguments: &[String],        cancellation_flag: Arc<AtomicBool>,        workspace: Option<WeakEntity<Workspace>>,        _: &mut Window,        cx: &mut App,    ) -> Task<Result<Vec<ArgumentCompletion>>> {        let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {            return Task::ready(Err(anyhow!("workspace was dropped")));        };        let path_style = workspace.read(cx).project().read(cx).path_style(cx);        let query = arguments.last().cloned().unwrap_or_default();         let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);        let executor = cx.background_executor().clone();        cx.background_spawn(async move {            let mut matches: Vec<String> = paths                .await                .into_iter()                .map(|path_match| {                    path_match                        .path_prefix                        .join(&path_match.path)                        .display(path_style)                        .to_string()                })                .collect();             matches.extend(                fuzzy::match_strings(                    &Options::match_candidates_for_args(),                    &query,                    false,                    true,                    10,                    &cancellation_flag,                    executor,                )                .await                .into_iter()                .map(|candidate| candidate.string),            );             Ok(matches                .into_iter()                .map(|completion| ArgumentCompletion {                    label: completion.clone().into(),                    new_text: completion,                    after_completion: assistant_slash_command::AfterCompletion::Run,                    replace_previous_arguments: false,                })                .collect())        })    }     fn run(        self: Arc<Self>,        arguments: &[String],        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],        _context_buffer: BufferSnapshot,        workspace: WeakEntity<Workspace>,        _delegate: Option<Arc<dyn LspAdapterDelegate>>,        window: &mut Window,        cx: &mut App,    ) -> Task<SlashCommandResult> {        let Some(workspace) = workspace.upgrade() else {            return Task::ready(Err(anyhow!("workspace was dropped")));        };         let project = workspace.read(cx).project();        let path_style = project.read(cx).path_style(cx);        let options = Options::parse(arguments, path_style);         let task = collect_diagnostics_output(project.clone(), options, cx);         window.spawn(cx, async move |_| {            task.await?                .map(|output| output.into_event_stream())                .context("No diagnostics found")        })    }} pub struct Options {    pub include_errors: bool,    pub include_warnings: bool,    pub path_matcher: Option<PathMatcher>,} const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings"; impl Options {    fn parse(arguments: &[String], path_style: PathStyle) -> Self {        let mut include_warnings = false;        let mut path_matcher = None;        for arg in arguments {            if arg == INCLUDE_WARNINGS_ARGUMENT {                include_warnings = true;            } else {                path_matcher = PathMatcher::new(&[arg.to_owned()], path_style).log_err();            }        }        Self {            include_errors: true,            include_warnings,            path_matcher,        }    }     fn match_candidates_for_args() -> [StringMatchCandidate; 1] {        [StringMatchCandidate::new(0, INCLUDE_WARNINGS_ARGUMENT)]    }} pub fn collect_diagnostics_output(    project: Entity<Project>,    options: Options,    cx: &mut App,) -> Task<Result<Option<SlashCommandOutput>>> {    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 output = SlashCommandOutput::default();         if let Some(error_source) = error_source.as_ref() {            writeln!(output.text, "diagnostics: {}", error_source).unwrap();        } else {            writeln!(output.text, "diagnostics").unwrap();        }         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 last_end = output.text.len();            let file_path = path.display(path_style).to_string();            if !glob_is_exact_file_match {                writeln!(&mut output.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());                collect_buffer_diagnostics(                    &mut output,                    &snapshot,                    options.include_warnings,                    options.include_errors,                );            }             if !glob_is_exact_file_match {                output.sections.push(SlashCommandOutputSection {                    range: last_end..output.text.len().saturating_sub(1),                    icon: IconName::File,                    label: file_path.into(),                    metadata: None,                });            }        }         // No diagnostics found        if output.sections.is_empty() {            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_str(",");                }            }             if project_summary.warning_count > 0 {                write!(label, " {} warnings", project_summary.warning_count).unwrap();            }        }         output.sections.insert(            0,            SlashCommandOutputSection {                range: 0..output.text.len(),                icon: IconName::Warning,                label: label.into(),                metadata: None,            },        );         Ok(Some(output))    })} pub fn collect_buffer_diagnostics(    output: &mut SlashCommandOutput,    snapshot: &BufferSnapshot,    include_warnings: bool,    include_errors: bool,) {    for (_, group) in snapshot.diagnostic_groups(None) {        let entry = &group.entries[group.primary_ix];        collect_diagnostic(output, entry, snapshot, include_warnings, include_errors)    }} fn collect_diagnostic(    output: &mut SlashCommandOutput,    entry: &DiagnosticEntryRef<'_, Anchor>,    snapshot: &BufferSnapshot,    include_warnings: bool,    include_errors: bool,) {    const EXCERPT_EXPANSION_SIZE: u32 = 2;    const MAX_MESSAGE_LENGTH: usize = 2000;     let (ty, icon) = match entry.diagnostic.severity {        DiagnosticSeverity::WARNING => {            if !include_warnings {                return;            }            ("warning", IconName::Warning)        }        DiagnosticSeverity::ERROR => {            if !include_errors {                return;            }            ("error", IconName::XCircle)        }        _ => return,    };    let prev_len = output.text.len();     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);     output.text.push_str("```");    if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {        output.text.push_str(&language_name);    }    output.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!(output.text, "{}", line).unwrap();         if line_number == diagnostic_row_number {            output.text.push_str("//");            let prev_len = output.text.len();            write!(output.text, " {}: ", ty).unwrap();            let padding = output.text.len() - prev_len;             let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH)                .replace('\n', format!("\n//{:padding$}", "").as_str());             writeln!(output.text, "{message}").unwrap();        }    }     writeln!(output.text, "```").unwrap();    output.sections.push(SlashCommandOutputSection {        range: prev_len..output.text.len().saturating_sub(1),        icon,        label: entry.diagnostic.message.clone().into(),        metadata: None,    });}