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    #[serde(deserialize_with = "deserialize_path")]
 29    pub path: Option<String>,
 30}
 31
 32fn deserialize_path<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
 33where
 34    D: serde::Deserializer<'de>,
 35{
 36    let opt = Option::<String>::deserialize(deserializer)?;
 37    // The model passes an empty string sometimes
 38    Ok(opt.filter(|s| !s.is_empty()))
 39}
 40
 41pub struct DiagnosticsTool;
 42
 43impl Tool for DiagnosticsTool {
 44    fn name(&self) -> String {
 45        "diagnostics".into()
 46    }
 47
 48    fn needs_confirmation(&self) -> bool {
 49        false
 50    }
 51
 52    fn description(&self) -> String {
 53        include_str!("./diagnostics_tool/description.md").into()
 54    }
 55
 56    fn icon(&self) -> IconName {
 57        IconName::Warning
 58    }
 59
 60    fn input_schema(&self) -> serde_json::Value {
 61        let schema = schemars::schema_for!(DiagnosticsToolInput);
 62        serde_json::to_value(&schema).unwrap()
 63    }
 64
 65    fn ui_text(&self, input: &serde_json::Value) -> String {
 66        if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
 67            .ok()
 68            .and_then(|input| match input.path {
 69                Some(path) if !path.is_empty() => Some(MarkdownString::escape(&path)),
 70                _ => None,
 71            })
 72        {
 73            format!("Check diagnostics for `{path}`")
 74        } else {
 75            "Check project diagnostics".to_string()
 76        }
 77    }
 78
 79    fn run(
 80        self: Arc<Self>,
 81        input: serde_json::Value,
 82        _messages: &[LanguageModelRequestMessage],
 83        project: Entity<Project>,
 84        action_log: Entity<ActionLog>,
 85        cx: &mut App,
 86    ) -> Task<Result<String>> {
 87        match serde_json::from_value::<DiagnosticsToolInput>(input)
 88            .ok()
 89            .and_then(|input| input.path)
 90        {
 91            Some(path) if !path.is_empty() => {
 92                let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
 93                    return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
 94                };
 95
 96                let buffer =
 97                    project.update(cx, |project, cx| project.open_buffer(project_path, cx));
 98
 99                cx.spawn(async move |cx| {
100                    let mut output = String::new();
101                    let buffer = buffer.await?;
102                    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
103
104                    for (_, group) in snapshot.diagnostic_groups(None) {
105                        let entry = &group.entries[group.primary_ix];
106                        let range = entry.range.to_point(&snapshot);
107                        let severity = match entry.diagnostic.severity {
108                            DiagnosticSeverity::ERROR => "error",
109                            DiagnosticSeverity::WARNING => "warning",
110                            _ => continue,
111                        };
112
113                        writeln!(
114                            output,
115                            "{} at line {}: {}",
116                            severity,
117                            range.start.row + 1,
118                            entry.diagnostic.message
119                        )?;
120                    }
121
122                    if output.is_empty() {
123                        Ok("File doesn't have errors or warnings!".to_string())
124                    } else {
125                        Ok(output)
126                    }
127                })
128            }
129            _ => {
130                let project = project.read(cx);
131                let mut output = String::new();
132                let mut has_diagnostics = false;
133
134                for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
135                    if summary.error_count > 0 || summary.warning_count > 0 {
136                        let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
137                        else {
138                            continue;
139                        };
140
141                        has_diagnostics = true;
142                        output.push_str(&format!(
143                            "{}: {} error(s), {} warning(s)\n",
144                            Path::new(worktree.read(cx).root_name())
145                                .join(project_path.path)
146                                .display(),
147                            summary.error_count,
148                            summary.warning_count
149                        ));
150                    }
151                }
152
153                action_log.update(cx, |action_log, _cx| {
154                    action_log.checked_project_diagnostics();
155                });
156
157                if has_diagnostics {
158                    Task::ready(Ok(output))
159                } else {
160                    Task::ready(Ok("No errors or warnings found in the project.".to_string()))
161                }
162            }
163        }
164    }
165}