@@ -40,6 +40,12 @@ pub enum MentionUri {
id: PromptId,
name: String,
},
+ Diagnostics {
+ #[serde(default = "default_include_errors")]
+ include_errors: bool,
+ #[serde(default)]
+ include_warnings: bool,
+ },
Selection {
#[serde(default, skip_serializing_if = "Option::is_none")]
abs_path: Option<PathBuf>,
@@ -135,6 +141,20 @@ impl MentionUri {
id: rule_id.into(),
name,
})
+ } else if path == "/agent/diagnostics" {
+ let mut include_errors = default_include_errors();
+ let mut include_warnings = false;
+ for (key, value) in url.query_pairs() {
+ match key.as_ref() {
+ "include_warnings" => include_warnings = value == "true",
+ "include_errors" => include_errors = value == "true",
+ _ => bail!("invalid query parameter"),
+ }
+ }
+ Ok(Self::Diagnostics {
+ include_errors,
+ include_warnings,
+ })
} else if path.starts_with("/agent/pasted-image") {
Ok(Self::PastedImage)
} else if path.starts_with("/agent/untitled-buffer") {
@@ -200,6 +220,7 @@ impl MentionUri {
MentionUri::Thread { name, .. } => name.clone(),
MentionUri::TextThread { name, .. } => name.clone(),
MentionUri::Rule { name, .. } => name.clone(),
+ MentionUri::Diagnostics { .. } => "Diagnostics".to_string(),
MentionUri::Selection {
abs_path: path,
line_range,
@@ -221,6 +242,7 @@ impl MentionUri {
MentionUri::Thread { .. } => IconName::Thread.path().into(),
MentionUri::TextThread { .. } => IconName::Thread.path().into(),
MentionUri::Rule { .. } => IconName::Reader.path().into(),
+ MentionUri::Diagnostics { .. } => IconName::Warning.path().into(),
MentionUri::Selection { .. } => IconName::Reader.path().into(),
MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(),
}
@@ -299,6 +321,21 @@ impl MentionUri {
url.query_pairs_mut().append_pair("name", name);
url
}
+ MentionUri::Diagnostics {
+ include_errors,
+ include_warnings,
+ } => {
+ let mut url = Url::parse("zed:///").unwrap();
+ url.set_path("/agent/diagnostics");
+ if *include_warnings {
+ url.query_pairs_mut()
+ .append_pair("include_warnings", "true");
+ }
+ if !include_errors {
+ url.query_pairs_mut().append_pair("include_errors", "false");
+ }
+ url
+ }
MentionUri::Fetch { url } => url.clone(),
}
}
@@ -312,6 +349,10 @@ impl fmt::Display for MentionLink<'_> {
}
}
+fn default_include_errors() -> bool {
+ true
+}
+
fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
let pairs = url.query_pairs().collect::<Vec<_>>();
match pairs.as_slice() {
@@ -504,6 +545,40 @@ mod tests {
assert_eq!(parsed.to_uri().to_string(), https_uri);
}
+ #[test]
+ fn test_parse_diagnostics_uri() {
+ let uri = "zed:///agent/diagnostics?include_warnings=true";
+ let parsed = MentionUri::parse(uri, PathStyle::local()).unwrap();
+ match &parsed {
+ MentionUri::Diagnostics {
+ include_errors,
+ include_warnings,
+ } => {
+ assert!(include_errors);
+ assert!(include_warnings);
+ }
+ _ => panic!("Expected Diagnostics variant"),
+ }
+ assert_eq!(parsed.to_uri().to_string(), uri);
+ }
+
+ #[test]
+ fn test_parse_diagnostics_uri_warnings_only() {
+ let uri = "zed:///agent/diagnostics?include_warnings=true&include_errors=false";
+ let parsed = MentionUri::parse(uri, PathStyle::local()).unwrap();
+ match &parsed {
+ MentionUri::Diagnostics {
+ include_errors,
+ include_warnings,
+ } => {
+ assert!(!include_errors);
+ assert!(include_warnings);
+ }
+ _ => panic!("Expected Diagnostics variant"),
+ }
+ assert_eq!(parsed.to_uri().to_string(), uri);
+ }
+
#[test]
fn test_invalid_scheme() {
assert!(MentionUri::parse("ftp://example.com", PathStyle::local()).is_err());
@@ -17,7 +17,7 @@ use lsp::CompletionContext;
use ordered_float::OrderedFloat;
use project::lsp_store::{CompletionDocumentation, SymbolLocation};
use project::{
- Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse,
+ Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, DiagnosticSummary,
PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId,
};
use prompt_store::{PromptStore, UserPromptId};
@@ -55,6 +55,7 @@ pub(crate) enum PromptContextType {
Fetch,
Thread,
Rules,
+ Diagnostics,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -92,6 +93,7 @@ impl TryFrom<&str> for PromptContextType {
"fetch" => Ok(Self::Fetch),
"thread" => Ok(Self::Thread),
"rule" => Ok(Self::Rules),
+ "diagnostics" => Ok(Self::Diagnostics),
_ => Err(format!("Invalid context picker mode: {}", value)),
}
}
@@ -105,6 +107,7 @@ impl PromptContextType {
Self::Fetch => "fetch",
Self::Thread => "thread",
Self::Rules => "rule",
+ Self::Diagnostics => "diagnostics",
}
}
@@ -115,6 +118,7 @@ impl PromptContextType {
Self::Fetch => "Fetch",
Self::Thread => "Threads",
Self::Rules => "Rules",
+ Self::Diagnostics => "Diagnostics",
}
}
@@ -125,6 +129,7 @@ impl PromptContextType {
Self::Fetch => IconName::ToolWeb,
Self::Thread => IconName::Thread,
Self::Rules => IconName::Reader,
+ Self::Diagnostics => IconName::Warning,
}
}
}
@@ -583,6 +588,103 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
})
}
+ fn completion_for_diagnostics(
+ source_range: Range<Anchor>,
+ source: Arc<T>,
+ editor: WeakEntity<Editor>,
+ mention_set: WeakEntity<MentionSet>,
+ workspace: Entity<Workspace>,
+ cx: &mut App,
+ ) -> Vec<Completion> {
+ let summary = workspace
+ .read(cx)
+ .project()
+ .read(cx)
+ .diagnostic_summary(false, cx);
+ if summary.error_count == 0 && summary.warning_count == 0 {
+ return Vec::new();
+ }
+ let icon_path = MentionUri::Diagnostics {
+ include_errors: true,
+ include_warnings: false,
+ }
+ .icon_path(cx);
+
+ let mut completions = Vec::new();
+
+ let cases = [
+ (summary.error_count > 0, true, false),
+ (summary.warning_count > 0, false, true),
+ (
+ summary.error_count > 0 && summary.warning_count > 0,
+ true,
+ true,
+ ),
+ ];
+
+ for (condition, include_errors, include_warnings) in cases {
+ if condition {
+ completions.push(Self::build_diagnostics_completion(
+ diagnostics_submenu_label(summary, include_errors, include_warnings),
+ source_range.clone(),
+ source.clone(),
+ editor.clone(),
+ mention_set.clone(),
+ workspace.clone(),
+ icon_path.clone(),
+ include_errors,
+ include_warnings,
+ summary,
+ ));
+ }
+ }
+
+ completions
+ }
+
+ fn build_diagnostics_completion(
+ menu_label: String,
+ source_range: Range<Anchor>,
+ source: Arc<T>,
+ editor: WeakEntity<Editor>,
+ mention_set: WeakEntity<MentionSet>,
+ workspace: Entity<Workspace>,
+ icon_path: SharedString,
+ include_errors: bool,
+ include_warnings: bool,
+ summary: DiagnosticSummary,
+ ) -> Completion {
+ let uri = MentionUri::Diagnostics {
+ include_errors,
+ include_warnings,
+ };
+ let crease_text = diagnostics_crease_label(summary, include_errors, include_warnings);
+ let display_text = format!("@{}", crease_text);
+ let new_text = format!("[{}]({}) ", display_text, uri.to_uri());
+ let new_text_len = new_text.len();
+ Completion {
+ replace_range: source_range.clone(),
+ new_text,
+ label: CodeLabel::plain(menu_label, None),
+ documentation: None,
+ source: project::CompletionSource::Custom,
+ icon_path: Some(icon_path),
+ match_start: None,
+ snippet_deduplication_key: None,
+ insert_text_mode: None,
+ confirm: Some(confirm_completion_callback(
+ crease_text,
+ source_range.start,
+ new_text_len - 1,
+ uri,
+ source,
+ editor,
+ mention_set,
+ workspace,
+ )),
+ }
+ }
+
fn search_slash_commands(&self, query: String, cx: &mut App) -> Task<Vec<AvailableCommand>> {
let commands = self.source.available_commands(cx);
if commands.is_empty() {
@@ -684,6 +786,8 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
}
}
+ Some(PromptContextType::Diagnostics) => Task::ready(Vec::new()),
+
None if query.is_empty() => {
let recent_task = self.recent_context_picker_entries(&workspace, cx);
let entries = self
@@ -879,6 +983,20 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
entries.push(PromptContextEntry::Mode(PromptContextType::Fetch));
}
+ if self
+ .source
+ .supports_context(PromptContextType::Diagnostics, cx)
+ {
+ let summary = workspace
+ .read(cx)
+ .project()
+ .read(cx)
+ .diagnostic_summary(false, cx);
+ if summary.error_count > 0 || summary.warning_count > 0 {
+ entries.push(PromptContextEntry::Mode(PromptContextType::Diagnostics));
+ }
+ }
+
entries
}
}
@@ -982,6 +1100,28 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
})
}
PromptCompletion::Mention(MentionCompletion { mode, argument, .. }) => {
+ if let Some(PromptContextType::Diagnostics) = mode {
+ if argument.is_some() {
+ return Task::ready(Ok(Vec::new()));
+ }
+
+ let completions = Self::completion_for_diagnostics(
+ source_range.clone(),
+ source.clone(),
+ editor.clone(),
+ mention_set.clone(),
+ workspace.clone(),
+ cx,
+ );
+ if !completions.is_empty() {
+ return Task::ready(Ok(vec![CompletionResponse {
+ completions,
+ display_options: CompletionDisplayOptions::default(),
+ is_incomplete: false,
+ }]));
+ }
+ }
+
let query = argument.unwrap_or_default();
let search_task =
self.search_mentions(mode, query, Arc::<AtomicBool>::default(), cx);
@@ -1051,7 +1191,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
cx,
)
}
-
Match::Symbol(SymbolMatch { symbol, .. }) => {
Self::completion_for_symbol(
symbol,
@@ -1064,7 +1203,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
cx,
)
}
-
Match::Thread(thread) => Some(Self::completion_for_thread(
thread,
source_range.clone(),
@@ -1075,7 +1213,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
workspace.clone(),
cx,
)),
-
Match::RecentThread(thread) => Some(Self::completion_for_thread(
thread,
source_range.clone(),
@@ -1086,7 +1223,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
workspace.clone(),
cx,
)),
-
Match::Rules(user_rules) => Some(Self::completion_for_rules(
user_rules,
source_range.clone(),
@@ -1096,7 +1232,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
workspace.clone(),
cx,
)),
-
Match::Fetch(url) => Self::completion_for_fetch(
source_range.clone(),
url,
@@ -1106,7 +1241,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
workspace.clone(),
cx,
),
-
Match::Entry(EntryMatch { entry, .. }) => {
Self::completion_for_entry(
entry,
@@ -1380,6 +1514,87 @@ impl MentionCompletion {
}
}
+fn diagnostics_label(
+ summary: DiagnosticSummary,
+ include_errors: bool,
+ include_warnings: bool,
+) -> String {
+ let mut parts = Vec::new();
+
+ if include_errors && summary.error_count > 0 {
+ parts.push(format!(
+ "{} {}",
+ summary.error_count,
+ pluralize("error", summary.error_count)
+ ));
+ }
+
+ if include_warnings && summary.warning_count > 0 {
+ parts.push(format!(
+ "{} {}",
+ summary.warning_count,
+ pluralize("warning", summary.warning_count)
+ ));
+ }
+
+ if parts.is_empty() {
+ return "Diagnostics".into();
+ }
+
+ let body = if parts.len() == 2 {
+ format!("{} and {}", parts[0], parts[1])
+ } else {
+ parts
+ .pop()
+ .expect("at least one part present after non-empty check")
+ };
+
+ format!("Diagnostics: {body}")
+}
+
+fn diagnostics_submenu_label(
+ summary: DiagnosticSummary,
+ include_errors: bool,
+ include_warnings: bool,
+) -> String {
+ match (include_errors, include_warnings) {
+ (true, true) => format!(
+ "{} {} & {} {}",
+ summary.error_count,
+ pluralize("error", summary.error_count),
+ summary.warning_count,
+ pluralize("warning", summary.warning_count)
+ ),
+ (true, _) => format!(
+ "{} {}",
+ summary.error_count,
+ pluralize("error", summary.error_count)
+ ),
+ (_, true) => format!(
+ "{} {}",
+ summary.warning_count,
+ pluralize("warning", summary.warning_count)
+ ),
+ _ => "Diagnostics".into(),
+ }
+}
+
+fn diagnostics_crease_label(
+ summary: DiagnosticSummary,
+ include_errors: bool,
+ include_warnings: bool,
+) -> SharedString {
+ diagnostics_label(summary, include_errors, include_warnings).into()
+}
+
+fn pluralize(noun: &str, count: usize) -> String {
+ if count == 1 {
+ noun.to_string()
+ } else {
+ format!("{noun}s")
+ }
+}
+
pub(crate) fn search_files(
query: String,
cancellation_flag: Arc<AtomicBool>,
@@ -1833,6 +2048,11 @@ mod tests {
#[test]
fn test_mention_completion_parse() {
let supported_modes = vec![PromptContextType::File, PromptContextType::Symbol];
+ let supported_modes_with_diagnostics = vec![
+ PromptContextType::File,
+ PromptContextType::Symbol,
+ PromptContextType::Diagnostics,
+ ];
assert_eq!(
MentionCompletion::try_parse("Lorem Ipsum", 0, &supported_modes),
@@ -1945,6 +2165,19 @@ mod tests {
})
);
+ assert_eq!(
+ MentionCompletion::try_parse(
+ "Lorem @diagnostics",
+ 0,
+ &supported_modes_with_diagnostics
+ ),
+ Some(MentionCompletion {
+ source_range: 6..18,
+ mode: Some(PromptContextType::Diagnostics),
+ argument: None,
+ })
+ );
+
// Disallowed non-file mentions
assert_eq!(
MentionCompletion::try_parse("Lorem @symbol main", 0, &[PromptContextType::File]),
@@ -188,7 +188,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
let path_style = project.read(cx).path_style(cx);
let options = Options::parse(arguments, path_style);
- let task = collect_diagnostics(project.clone(), options, cx);
+ let task = collect_diagnostics_output(project.clone(), options, cx);
window.spawn(cx, async move |_| {
task.await?
@@ -198,10 +198,10 @@ impl SlashCommand for DiagnosticsSlashCommand {
}
}
-#[derive(Default)]
-struct Options {
- include_warnings: bool,
- path_matcher: Option<PathMatcher>,
+pub struct Options {
+ pub include_errors: bool,
+ pub include_warnings: bool,
+ pub path_matcher: Option<PathMatcher>,
}
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
@@ -218,6 +218,7 @@ impl Options {
}
}
Self {
+ include_errors: true,
include_warnings,
path_matcher,
}
@@ -228,7 +229,7 @@ impl Options {
}
}
-fn collect_diagnostics(
+pub fn collect_diagnostics_output(
project: Entity<Project>,
options: Options,
cx: &mut App,
@@ -282,11 +283,17 @@ fn collect_diagnostics(
continue;
}
- project_summary.error_count += summary.error_count;
+ 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;
- } else if summary.error_count == 0 {
- continue;
}
let last_end = output.text.len();
@@ -301,7 +308,12 @@ fn collect_diagnostics(
.log_err()
{
let snapshot = cx.read_entity(&buffer, |buffer, _| buffer.snapshot());
- collect_buffer_diagnostics(&mut output, &snapshot, options.include_warnings);
+ collect_buffer_diagnostics(
+ &mut output,
+ &snapshot,
+ options.include_warnings,
+ options.include_errors,
+ );
}
if !glob_is_exact_file_match {
@@ -358,10 +370,11 @@ 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)
+ collect_diagnostic(output, entry, snapshot, include_warnings, include_errors)
}
}
@@ -370,6 +383,7 @@ fn collect_diagnostic(
entry: &DiagnosticEntryRef<'_, Anchor>,
snapshot: &BufferSnapshot,
include_warnings: bool,
+ include_errors: bool,
) {
const EXCERPT_EXPANSION_SIZE: u32 = 2;
const MAX_MESSAGE_LENGTH: usize = 2000;
@@ -381,7 +395,12 @@ fn collect_diagnostic(
}
("warning", IconName::Warning)
}
- DiagnosticSeverity::ERROR => ("error", IconName::XCircle),
+ DiagnosticSeverity::ERROR => {
+ if !include_errors {
+ return;
+ }
+ ("error", IconName::XCircle)
+ }
_ => return,
};
let prev_len = output.text.len();