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