diagnostics_tool.rs

  1use anyhow::{anyhow, Result};
  2use assistant_tool::{ActionLog, Tool};
  3use gpui::{App, Entity, Task};
  4use language::{DiagnosticSeverity, OffsetRangeExt};
  5use language_model::LanguageModelRequestMessage;
  6use project::Project;
  7use schemars::JsonSchema;
  8use serde::{Deserialize, Serialize};
  9use std::{fmt::Write, path::Path, sync::Arc};
 10use ui::IconName;
 11use util::markdown::MarkdownString;
 12
 13#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 14pub struct DiagnosticsToolInput {
 15    /// The path to get diagnostics for. If not provided, returns a project-wide summary.
 16    ///
 17    /// This path should never be absolute, and the first component
 18    /// of the path should always be a root directory in a project.
 19    ///
 20    /// <example>
 21    /// If the project has the following root directories:
 22    ///
 23    /// - lorem
 24    /// - ipsum
 25    ///
 26    /// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
 27    /// </example>
 28    pub path: Option<String>,
 29}
 30
 31pub struct DiagnosticsTool;
 32
 33impl Tool for DiagnosticsTool {
 34    fn name(&self) -> String {
 35        "diagnostics".into()
 36    }
 37
 38    fn needs_confirmation(&self) -> bool {
 39        false
 40    }
 41
 42    fn description(&self) -> String {
 43        include_str!("./diagnostics_tool/description.md").into()
 44    }
 45
 46    fn icon(&self) -> IconName {
 47        IconName::Warning
 48    }
 49
 50    fn input_schema(&self) -> serde_json::Value {
 51        let schema = schemars::schema_for!(DiagnosticsToolInput);
 52        serde_json::to_value(&schema).unwrap()
 53    }
 54
 55    fn ui_text(&self, input: &serde_json::Value) -> String {
 56        if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
 57            .ok()
 58            .and_then(|input| match input.path {
 59                Some(path) if !path.is_empty() => Some(MarkdownString::escape(&path)),
 60                _ => None,
 61            })
 62        {
 63            format!("Check diagnostics for `{path}`")
 64        } else {
 65            "Check project diagnostics".to_string()
 66        }
 67    }
 68
 69    fn run(
 70        self: Arc<Self>,
 71        input: serde_json::Value,
 72        _messages: &[LanguageModelRequestMessage],
 73        project: Entity<Project>,
 74        _action_log: Entity<ActionLog>,
 75        cx: &mut App,
 76    ) -> Task<Result<String>> {
 77        match serde_json::from_value::<DiagnosticsToolInput>(input)
 78            .ok()
 79            .and_then(|input| input.path)
 80        {
 81            Some(path) if !path.is_empty() => {
 82                let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
 83                    return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
 84                };
 85
 86                let buffer =
 87                    project.update(cx, |project, cx| project.open_buffer(project_path, cx));
 88
 89                cx.spawn(async move |cx| {
 90                    let mut output = String::new();
 91                    let buffer = buffer.await?;
 92                    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
 93
 94                    for (_, group) in snapshot.diagnostic_groups(None) {
 95                        let entry = &group.entries[group.primary_ix];
 96                        let range = entry.range.to_point(&snapshot);
 97                        let severity = match entry.diagnostic.severity {
 98                            DiagnosticSeverity::ERROR => "error",
 99                            DiagnosticSeverity::WARNING => "warning",
100                            _ => continue,
101                        };
102
103                        writeln!(
104                            output,
105                            "{} at line {}: {}",
106                            severity,
107                            range.start.row + 1,
108                            entry.diagnostic.message
109                        )?;
110                    }
111
112                    if output.is_empty() {
113                        Ok("File doesn't have errors or warnings!".to_string())
114                    } else {
115                        Ok(output)
116                    }
117                })
118            }
119            _ => {
120                let project = project.read(cx);
121                let mut output = String::new();
122                let mut has_diagnostics = false;
123
124                for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
125                    if summary.error_count > 0 || summary.warning_count > 0 {
126                        let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
127                        else {
128                            continue;
129                        };
130
131                        has_diagnostics = true;
132                        output.push_str(&format!(
133                            "{}: {} error(s), {} warning(s)\n",
134                            Path::new(worktree.read(cx).root_name())
135                                .join(project_path.path)
136                                .display(),
137                            summary.error_count,
138                            summary.warning_count
139                        ));
140                    }
141                }
142
143                if has_diagnostics {
144                    Task::ready(Ok(output))
145                } else {
146                    Task::ready(Ok("No errors or warnings found in the project.".to_string()))
147                }
148            }
149        }
150    }
151}