Introduce a new `/delta` command (#17903)

Antonio Scandurra and Roy created

Release Notes:

- Added a new `/delta` command to re-insert changed files that were
previously included in a context.

---------

Co-authored-by: Roy <roy@anthropic.com>

Change summary

Cargo.lock                                                    |   1 
crates/assistant/src/assistant.rs                             |   7 
crates/assistant/src/assistant_panel.rs                       |  17 
crates/assistant/src/context.rs                               |  15 
crates/assistant/src/context/context_tests.rs                 |   5 
crates/assistant/src/slash_command.rs                         |   1 
crates/assistant/src/slash_command/auto_command.rs            |   4 
crates/assistant/src/slash_command/context_server_command.rs  |   6 
crates/assistant/src/slash_command/default_command.rs         |   5 
crates/assistant/src/slash_command/delta_command.rs           | 109 ++
crates/assistant/src/slash_command/diagnostics_command.rs     | 234 +---
crates/assistant/src/slash_command/docs_command.rs            |   5 
crates/assistant/src/slash_command/fetch_command.rs           |   5 
crates/assistant/src/slash_command/file_command.rs            | 260 ++--
crates/assistant/src/slash_command/now_command.rs             |   5 
crates/assistant/src/slash_command/project_command.rs         |   5 
crates/assistant/src/slash_command/prompt_command.rs          |   5 
crates/assistant/src/slash_command/search_command.rs          |   3 
crates/assistant/src/slash_command/symbols_command.rs         |   5 
crates/assistant/src/slash_command/tab_command.rs             |  47 
crates/assistant/src/slash_command/terminal_command.rs        |   5 
crates/assistant/src/slash_command/workflow_command.rs        |   5 
crates/assistant_slash_command/Cargo.toml                     |   1 
crates/assistant_slash_command/src/assistant_slash_command.rs |  13 
crates/extension/src/extension_slash_command.rs               |   5 
crates/proto/proto/zed.proto                                  |   1 
26 files changed, 408 insertions(+), 366 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -455,6 +455,7 @@ dependencies = [
  "language",
  "parking_lot",
  "serde",
+ "serde_json",
  "workspace",
 ]
 

crates/assistant/src/assistant.rs 🔗

@@ -41,9 +41,9 @@ use semantic_index::{CloudEmbeddingProvider, SemanticDb};
 use serde::{Deserialize, Serialize};
 use settings::{update_settings_file, Settings, SettingsStore};
 use slash_command::{
-    auto_command, context_server_command, default_command, diagnostics_command, docs_command,
-    fetch_command, file_command, now_command, project_command, prompt_command, search_command,
-    symbols_command, tab_command, terminal_command, workflow_command,
+    auto_command, context_server_command, default_command, delta_command, diagnostics_command,
+    docs_command, fetch_command, file_command, now_command, project_command, prompt_command,
+    search_command, symbols_command, tab_command, terminal_command, workflow_command,
 };
 use std::path::PathBuf;
 use std::sync::Arc;
@@ -367,6 +367,7 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
     let slash_command_registry = SlashCommandRegistry::global(cx);
 
     slash_command_registry.register_command(file_command::FileSlashCommand, true);
+    slash_command_registry.register_command(delta_command::DeltaSlashCommand, true);
     slash_command_registry.register_command(symbols_command::OutlineSlashCommand, true);
     slash_command_registry.register_command(tab_command::TabSlashCommand, true);
     slash_command_registry.register_command(project_command::ProjectSlashCommand, true);

crates/assistant/src/assistant_panel.rs 🔗

@@ -1906,7 +1906,22 @@ impl ContextEditor {
         cx: &mut ViewContext<Self>,
     ) {
         if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
-            let output = command.run(arguments, workspace, self.lsp_adapter_delegate.clone(), cx);
+            let context = self.context.read(cx);
+            let sections = context
+                .slash_command_output_sections()
+                .into_iter()
+                .filter(|section| section.is_valid(context.buffer().read(cx)))
+                .cloned()
+                .collect::<Vec<_>>();
+            let snapshot = context.buffer().read(cx).snapshot();
+            let output = command.run(
+                arguments,
+                &sections,
+                snapshot,
+                workspace,
+                self.lsp_adapter_delegate.clone(),
+                cx,
+            );
             self.context.update(cx, |context, cx| {
                 context.insert_command_output(
                     command_range,

crates/assistant/src/context.rs 🔗

@@ -48,7 +48,7 @@ use std::{
 };
 use telemetry_events::AssistantKind;
 use text::BufferSnapshot;
-use util::{post_inc, TryFutureExt};
+use util::{post_inc, ResultExt, TryFutureExt};
 use uuid::Uuid;
 
 #[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
@@ -162,6 +162,9 @@ impl ContextOperation {
                                 )?,
                                 icon: section.icon_name.parse()?,
                                 label: section.label.into(),
+                                metadata: section
+                                    .metadata
+                                    .and_then(|metadata| serde_json::from_str(&metadata).log_err()),
                             })
                         })
                         .collect::<Result<Vec<_>>>()?,
@@ -242,6 +245,9 @@ impl ContextOperation {
                                     )),
                                     icon_name: icon_name.to_string(),
                                     label: section.label.to_string(),
+                                    metadata: section.metadata.as_ref().and_then(|metadata| {
+                                        serde_json::to_string(metadata).log_err()
+                                    }),
                                 }
                             })
                             .collect(),
@@ -635,12 +641,13 @@ impl Context {
                 .slash_command_output_sections
                 .iter()
                 .filter_map(|section| {
-                    let range = section.range.to_offset(buffer);
-                    if section.range.start.is_valid(buffer) && !range.is_empty() {
+                    if section.is_valid(buffer) {
+                        let range = section.range.to_offset(buffer);
                         Some(assistant_slash_command::SlashCommandOutputSection {
                             range,
                             icon: section.icon,
                             label: section.label.clone(),
+                            metadata: section.metadata.clone(),
                         })
                     } else {
                         None
@@ -1825,6 +1832,7 @@ impl Context {
                                         ..buffer.anchor_before(start + section.range.end),
                                     icon: section.icon,
                                     label: section.label,
+                                    metadata: section.metadata,
                                 })
                                 .collect::<Vec<_>>();
                             sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
@@ -2977,6 +2985,7 @@ impl SavedContext {
                             ..buffer.anchor_before(section.range.end),
                         icon: section.icon,
                         label: section.label,
+                        metadata: section.metadata,
                     }
                 })
                 .collect(),

crates/assistant/src/context/context_tests.rs 🔗

@@ -12,7 +12,7 @@ use assistant_slash_command::{
 use collections::HashSet;
 use fs::FakeFs;
 use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
-use language::{Buffer, LanguageRegistry, LspAdapterDelegate};
+use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
 use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
 use parking_lot::Mutex;
 use project::Project;
@@ -1089,6 +1089,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
                             range: section_start..section_end,
                             icon: ui::IconName::Ai,
                             label: "section".into(),
+                            metadata: None,
                         });
                     }
 
@@ -1425,6 +1426,8 @@ impl SlashCommand for FakeSlashCommand {
     fn run(
         self: Arc<Self>,
         _arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         _workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         _cx: &mut WindowContext,

crates/assistant/src/slash_command.rs 🔗

@@ -22,6 +22,7 @@ use workspace::Workspace;
 pub mod auto_command;
 pub mod context_server_command;
 pub mod default_command;
+pub mod delta_command;
 pub mod diagnostics_command;
 pub mod docs_command;
 pub mod fetch_command;

crates/assistant/src/slash_command/auto_command.rs 🔗

@@ -1,7 +1,7 @@
 use super::create_label_for_command;
 use super::{SlashCommand, SlashCommandOutput};
 use anyhow::{anyhow, Result};
-use assistant_slash_command::ArgumentCompletion;
+use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
 use feature_flags::FeatureFlag;
 use futures::StreamExt;
 use gpui::{AppContext, AsyncAppContext, Task, WeakView};
@@ -87,6 +87,8 @@ impl SlashCommand for AutoCommand {
     fn run(
         self: Arc<Self>,
         arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: language::BufferSnapshot,
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,

crates/assistant/src/slash_command/context_server_command.rs 🔗

@@ -9,7 +9,7 @@ use context_servers::{
     protocol::PromptInfo,
 };
 use gpui::{Task, WeakView, WindowContext};
-use language::{CodeLabel, LspAdapterDelegate};
+use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
 use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
 use text::LineEnding;
@@ -96,7 +96,6 @@ impl SlashCommand for ContextServerSlashCommand {
                         replace_previous_arguments: false,
                     })
                     .collect();
-
                 Ok(completions)
             })
         } else {
@@ -107,6 +106,8 @@ impl SlashCommand for ContextServerSlashCommand {
     fn run(
         self: Arc<Self>,
         arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         _workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -141,6 +142,7 @@ impl SlashCommand for ContextServerSlashCommand {
                                 .description
                                 .unwrap_or(format!("Result from {}", prompt_name)),
                         ),
+                        metadata: None,
                     }],
                     text: prompt,
                     run_commands_in_text: false,

crates/assistant/src/slash_command/default_command.rs 🔗

@@ -3,7 +3,7 @@ use crate::prompt_library::PromptStore;
 use anyhow::{anyhow, Result};
 use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
 use gpui::{Task, WeakView};
-use language::LspAdapterDelegate;
+use language::{BufferSnapshot, LspAdapterDelegate};
 use std::{
     fmt::Write,
     sync::{atomic::AtomicBool, Arc},
@@ -43,6 +43,8 @@ impl SlashCommand for DefaultSlashCommand {
     fn run(
         self: Arc<Self>,
         _arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         _workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -70,6 +72,7 @@ impl SlashCommand for DefaultSlashCommand {
                     range: 0..text.len(),
                     icon: IconName::Library,
                     label: "Default".into(),
+                    metadata: None,
                 }],
                 text,
                 run_commands_in_text: true,

crates/assistant/src/slash_command/delta_command.rs 🔗

@@ -0,0 +1,109 @@
+use crate::slash_command::file_command::{FileCommandMetadata, FileSlashCommand};
+use anyhow::Result;
+use assistant_slash_command::{
+    ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
+};
+use collections::HashSet;
+use futures::future;
+use gpui::{Task, WeakView, WindowContext};
+use language::{BufferSnapshot, LspAdapterDelegate};
+use std::sync::{atomic::AtomicBool, Arc};
+use text::OffsetRangeExt;
+use workspace::Workspace;
+
+pub(crate) struct DeltaSlashCommand;
+
+impl SlashCommand for DeltaSlashCommand {
+    fn name(&self) -> String {
+        "delta".into()
+    }
+
+    fn description(&self) -> String {
+        "re-insert changed files".into()
+    }
+
+    fn menu_text(&self) -> String {
+        "Re-insert Changed Files".into()
+    }
+
+    fn requires_argument(&self) -> bool {
+        false
+    }
+
+    fn complete_argument(
+        self: Arc<Self>,
+        _arguments: &[String],
+        _cancellation_flag: Arc<AtomicBool>,
+        _workspace: Option<WeakView<Workspace>>,
+        _cx: &mut WindowContext,
+    ) -> Task<Result<Vec<ArgumentCompletion>>> {
+        unimplemented!()
+    }
+
+    fn run(
+        self: Arc<Self>,
+        _arguments: &[String],
+        context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        context_buffer: BufferSnapshot,
+        workspace: WeakView<Workspace>,
+        delegate: Option<Arc<dyn LspAdapterDelegate>>,
+        cx: &mut WindowContext,
+    ) -> Task<Result<SlashCommandOutput>> {
+        let mut paths = HashSet::default();
+        let mut file_command_old_outputs = Vec::new();
+        let mut file_command_new_outputs = Vec::new();
+        for section in context_slash_command_output_sections.iter().rev() {
+            if let Some(metadata) = section
+                .metadata
+                .as_ref()
+                .and_then(|value| serde_json::from_value::<FileCommandMetadata>(value.clone()).ok())
+            {
+                if paths.insert(metadata.path.clone()) {
+                    file_command_old_outputs.push(
+                        context_buffer
+                            .as_rope()
+                            .slice(section.range.to_offset(&context_buffer)),
+                    );
+                    file_command_new_outputs.push(Arc::new(FileSlashCommand).run(
+                        &[metadata.path.clone()],
+                        context_slash_command_output_sections,
+                        context_buffer.clone(),
+                        workspace.clone(),
+                        delegate.clone(),
+                        cx,
+                    ));
+                }
+            }
+        }
+
+        cx.background_executor().spawn(async move {
+            let mut output = SlashCommandOutput::default();
+
+            let file_command_new_outputs = future::join_all(file_command_new_outputs).await;
+            for (old_text, new_output) in file_command_old_outputs
+                .into_iter()
+                .zip(file_command_new_outputs)
+            {
+                if let Ok(new_output) = new_output {
+                    if let Some(file_command_range) = new_output.sections.first() {
+                        let new_text = &new_output.text[file_command_range.range.clone()];
+                        if old_text.chars().ne(new_text.chars()) {
+                            output.sections.extend(new_output.sections.into_iter().map(
+                                |section| SlashCommandOutputSection {
+                                    range: output.text.len() + section.range.start
+                                        ..output.text.len() + section.range.end,
+                                    icon: section.icon,
+                                    label: section.label,
+                                    metadata: section.metadata,
+                                },
+                            ));
+                            output.text.push_str(&new_output.text);
+                        }
+                    }
+                }
+            }
+
+            Ok(output)
+        })
+    }
+}

crates/assistant/src/slash_command/diagnostics_command.rs 🔗

@@ -9,10 +9,9 @@ use language::{
 };
 use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
 use rope::Point;
-use std::fmt::Write;
-use std::path::{Path, PathBuf};
 use std::{
-    ops::Range,
+    fmt::Write,
+    path::{Path, PathBuf},
     sync::{atomic::AtomicBool, Arc},
 };
 use ui::prelude::*;
@@ -163,6 +162,8 @@ impl SlashCommand for DiagnosticsSlashCommand {
     fn run(
         self: Arc<Self>,
         arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -175,68 +176,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
 
         let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
 
-        cx.spawn(move |_| async move {
-            let Some((text, sections)) = task.await? else {
-                return Ok(SlashCommandOutput {
-                    sections: vec![SlashCommandOutputSection {
-                        range: 0..1,
-                        icon: IconName::Library,
-                        label: "No Diagnostics".into(),
-                    }],
-                    text: "\n".to_string(),
-                    run_commands_in_text: true,
-                });
-            };
-
-            let sections = sections
-                .into_iter()
-                .map(|(range, placeholder_type)| SlashCommandOutputSection {
-                    range,
-                    icon: match placeholder_type {
-                        PlaceholderType::Root(_, _) => IconName::Warning,
-                        PlaceholderType::File(_) => IconName::File,
-                        PlaceholderType::Diagnostic(DiagnosticType::Error, _) => IconName::XCircle,
-                        PlaceholderType::Diagnostic(DiagnosticType::Warning, _) => {
-                            IconName::Warning
-                        }
-                    },
-                    label: match placeholder_type {
-                        PlaceholderType::Root(summary, source) => {
-                            let mut label = String::new();
-                            label.push_str("Diagnostics");
-                            if let Some(source) = source {
-                                write!(label, " ({})", source).unwrap();
-                            }
-
-                            if summary.error_count > 0 || summary.warning_count > 0 {
-                                label.push(':');
-
-                                if summary.error_count > 0 {
-                                    write!(label, " {} errors", summary.error_count).unwrap();
-                                    if summary.warning_count > 0 {
-                                        label.push_str(",");
-                                    }
-                                }
-
-                                if summary.warning_count > 0 {
-                                    write!(label, " {} warnings", summary.warning_count).unwrap();
-                                }
-                            }
-
-                            label.into()
-                        }
-                        PlaceholderType::File(file_path) => file_path.into(),
-                        PlaceholderType::Diagnostic(_, message) => message.into(),
-                    },
-                })
-                .collect();
-
-            Ok(SlashCommandOutput {
-                text,
-                sections,
-                run_commands_in_text: false,
-            })
-        })
+        cx.spawn(move |_| async move { task.await?.ok_or_else(|| anyhow!("No diagnostics found")) })
     }
 }
 
@@ -277,7 +217,7 @@ fn collect_diagnostics(
     project: Model<Project>,
     options: Options,
     cx: &mut AppContext,
-) -> Task<Result<Option<(String, Vec<(Range<usize>, PlaceholderType)>)>>> {
+) -> Task<Result<Option<SlashCommandOutput>>> {
     let error_source = if let Some(path_matcher) = &options.path_matcher {
         debug_assert_eq!(path_matcher.sources().len(), 1);
         Some(path_matcher.sources().first().cloned().unwrap_or_default())
@@ -318,13 +258,13 @@ fn collect_diagnostics(
         .collect();
 
     cx.spawn(|mut cx| async move {
-        let mut text = String::new();
+        let mut output = SlashCommandOutput::default();
+
         if let Some(error_source) = error_source.as_ref() {
-            writeln!(text, "diagnostics: {}", error_source).unwrap();
+            writeln!(output.text, "diagnostics: {}", error_source).unwrap();
         } else {
-            writeln!(text, "diagnostics").unwrap();
+            writeln!(output.text, "diagnostics").unwrap();
         }
-        let mut sections: Vec<(Range<usize>, PlaceholderType)> = Vec::new();
 
         let mut project_summary = DiagnosticSummary::default();
         for (project_path, path, summary) in diagnostic_summaries {
@@ -341,10 +281,10 @@ fn collect_diagnostics(
                 continue;
             }
 
-            let last_end = text.len();
+            let last_end = output.text.len();
             let file_path = path.to_string_lossy().to_string();
             if !glob_is_exact_file_match {
-                writeln!(&mut text, "{file_path}").unwrap();
+                writeln!(&mut output.text, "{file_path}").unwrap();
             }
 
             if let Some(buffer) = project_handle
@@ -352,75 +292,73 @@ fn collect_diagnostics(
                 .await
                 .log_err()
             {
-                collect_buffer_diagnostics(
-                    &mut text,
-                    &mut sections,
-                    cx.read_model(&buffer, |buffer, _| buffer.snapshot())?,
-                    options.include_warnings,
-                );
+                let snapshot = cx.read_model(&buffer, |buffer, _| buffer.snapshot())?;
+                collect_buffer_diagnostics(&mut output, &snapshot, options.include_warnings);
             }
 
             if !glob_is_exact_file_match {
-                sections.push((
-                    last_end..text.len().saturating_sub(1),
-                    PlaceholderType::File(file_path),
-                ))
+                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 sections.is_empty() {
+        if output.sections.is_empty() {
             return Ok(None);
         }
 
-        sections.push((
-            0..text.len(),
-            PlaceholderType::Root(project_summary, error_source),
-        ));
-        Ok(Some((text, sections)))
-    })
-}
-
-pub fn buffer_has_error_diagnostics(snapshot: &BufferSnapshot) -> bool {
-    for (_, group) in snapshot.diagnostic_groups(None) {
-        let entry = &group.entries[group.primary_ix];
-        if entry.diagnostic.severity == DiagnosticSeverity::ERROR {
-            return true;
+        let mut label = String::new();
+        label.push_str("Diagnostics");
+        if let Some(source) = error_source {
+            write!(label, " ({})", source).unwrap();
         }
-    }
-    false
-}
 
-pub fn write_single_file_diagnostics(
-    output: &mut String,
-    path: Option<&Path>,
-    snapshot: &BufferSnapshot,
-) -> bool {
-    if let Some(path) = path {
-        if buffer_has_error_diagnostics(&snapshot) {
-            output.push_str("/diagnostics ");
-            output.push_str(&path.to_string_lossy());
-            return true;
+        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();
+            }
         }
-    }
-    false
+
+        output.sections.insert(
+            0,
+            SlashCommandOutputSection {
+                range: 0..output.text.len(),
+                icon: IconName::Warning,
+                label: label.into(),
+                metadata: None,
+            },
+        );
+
+        Ok(Some(output))
+    })
 }
 
-fn collect_buffer_diagnostics(
-    text: &mut String,
-    sections: &mut Vec<(Range<usize>, PlaceholderType)>,
-    snapshot: BufferSnapshot,
+pub fn collect_buffer_diagnostics(
+    output: &mut SlashCommandOutput,
+    snapshot: &BufferSnapshot,
     include_warnings: bool,
 ) {
     for (_, group) in snapshot.diagnostic_groups(None) {
         let entry = &group.entries[group.primary_ix];
-        collect_diagnostic(text, sections, entry, &snapshot, include_warnings)
+        collect_diagnostic(output, entry, &snapshot, include_warnings)
     }
 }
 
 fn collect_diagnostic(
-    text: &mut String,
-    sections: &mut Vec<(Range<usize>, PlaceholderType)>,
+    output: &mut SlashCommandOutput,
     entry: &DiagnosticEntry<Anchor>,
     snapshot: &BufferSnapshot,
     include_warnings: bool,
@@ -428,17 +366,17 @@ fn collect_diagnostic(
     const EXCERPT_EXPANSION_SIZE: u32 = 2;
     const MAX_MESSAGE_LENGTH: usize = 2000;
 
-    let ty = match entry.diagnostic.severity {
+    let (ty, icon) = match entry.diagnostic.severity {
         DiagnosticSeverity::WARNING => {
             if !include_warnings {
                 return;
             }
-            DiagnosticType::Warning
+            ("warning", IconName::Warning)
         }
-        DiagnosticSeverity::ERROR => DiagnosticType::Error,
+        DiagnosticSeverity::ERROR => ("error", IconName::XCircle),
         _ => return,
     };
-    let prev_len = text.len();
+    let prev_len = output.text.len();
 
     let range = entry.range.to_point(snapshot);
     let diagnostic_row_number = range.start.row + 1;
@@ -448,11 +386,11 @@ fn collect_diagnostic(
     let excerpt_range =
         Point::new(start_row, 0).to_offset(&snapshot)..Point::new(end_row, 0).to_offset(&snapshot);
 
-    text.push_str("```");
+    output.text.push_str("```");
     if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {
-        text.push_str(&language_name);
+        output.text.push_str(&language_name);
     }
-    text.push('\n');
+    output.text.push('\n');
 
     let mut buffer_text = String::new();
     for chunk in snapshot.text_for_range(excerpt_range) {
@@ -461,46 +399,26 @@ fn collect_diagnostic(
 
     for (i, line) in buffer_text.lines().enumerate() {
         let line_number = start_row + i as u32 + 1;
-        writeln!(text, "{}", line).unwrap();
+        writeln!(output.text, "{}", line).unwrap();
 
         if line_number == diagnostic_row_number {
-            text.push_str("//");
-            let prev_len = text.len();
-            write!(text, " {}: ", ty.as_str()).unwrap();
-            let padding = text.len() - prev_len;
+            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!(text, "{message}").unwrap();
+            writeln!(output.text, "{message}").unwrap();
         }
     }
 
-    writeln!(text, "```").unwrap();
-    sections.push((
-        prev_len..text.len().saturating_sub(1),
-        PlaceholderType::Diagnostic(ty, entry.diagnostic.message.clone()),
-    ))
-}
-
-#[derive(Clone)]
-pub enum PlaceholderType {
-    Root(DiagnosticSummary, Option<String>),
-    File(String),
-    Diagnostic(DiagnosticType, String),
-}
-
-#[derive(Copy, Clone)]
-pub enum DiagnosticType {
-    Warning,
-    Error,
-}
-
-impl DiagnosticType {
-    pub fn as_str(&self) -> &'static str {
-        match self {
-            DiagnosticType::Warning => "warning",
-            DiagnosticType::Error => "error",
-        }
-    }
+    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,
+    });
 }

crates/assistant/src/slash_command/docs_command.rs 🔗

@@ -12,7 +12,7 @@ use indexed_docs::{
     DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
     ProviderId,
 };
-use language::LspAdapterDelegate;
+use language::{BufferSnapshot, LspAdapterDelegate};
 use project::{Project, ProjectPath};
 use ui::prelude::*;
 use util::{maybe, ResultExt};
@@ -269,6 +269,8 @@ impl SlashCommand for DocsSlashCommand {
     fn run(
         self: Arc<Self>,
         arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         _workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -349,6 +351,7 @@ impl SlashCommand for DocsSlashCommand {
                         range,
                         icon: IconName::FileDoc,
                         label: format!("docs ({provider}): {key}",).into(),
+                        metadata: None,
                     })
                     .collect(),
                 run_commands_in_text: false,

crates/assistant/src/slash_command/fetch_command.rs 🔗

@@ -11,7 +11,7 @@ use futures::AsyncReadExt;
 use gpui::{Task, WeakView};
 use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
 use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
-use language::LspAdapterDelegate;
+use language::{BufferSnapshot, LspAdapterDelegate};
 use ui::prelude::*;
 use workspace::Workspace;
 
@@ -128,6 +128,8 @@ impl SlashCommand for FetchSlashCommand {
     fn run(
         self: Arc<Self>,
         arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -161,6 +163,7 @@ impl SlashCommand for FetchSlashCommand {
                     range,
                     icon: IconName::AtSign,
                     label: format!("fetch {}", url).into(),
+                    metadata: None,
                 }],
                 run_commands_in_text: false,
             })

crates/assistant/src/slash_command/file_command.rs 🔗

@@ -1,10 +1,11 @@
-use super::{diagnostics_command::write_single_file_diagnostics, SlashCommand, SlashCommandOutput};
+use super::{diagnostics_command::collect_buffer_diagnostics, SlashCommand, SlashCommandOutput};
 use anyhow::{anyhow, Context as _, Result};
 use assistant_slash_command::{AfterCompletion, ArgumentCompletion, SlashCommandOutputSection};
 use fuzzy::PathMatch;
 use gpui::{AppContext, Model, Task, View, WeakView};
 use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
 use project::{PathMatchCandidateSet, Project};
+use serde::{Deserialize, Serialize};
 use std::{
     fmt::Write,
     ops::Range,
@@ -175,6 +176,8 @@ impl SlashCommand for FileSlashCommand {
     fn run(
         self: Arc<Self>,
         arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -187,54 +190,15 @@ impl SlashCommand for FileSlashCommand {
             return Task::ready(Err(anyhow!("missing path")));
         };
 
-        let task = collect_files(workspace.read(cx).project().clone(), arguments, cx);
-
-        cx.foreground_executor().spawn(async move {
-            let output = task.await?;
-            Ok(SlashCommandOutput {
-                text: output.completion_text,
-                sections: output
-                    .files
-                    .into_iter()
-                    .map(|file| {
-                        build_entry_output_section(
-                            file.range_in_text,
-                            Some(&file.path),
-                            file.entry_type == EntryType::Directory,
-                            None,
-                        )
-                    })
-                    .collect(),
-                run_commands_in_text: true,
-            })
-        })
+        collect_files(workspace.read(cx).project().clone(), arguments, cx)
     }
 }
 
-#[derive(Clone, Copy, PartialEq, Debug)]
-enum EntryType {
-    File,
-    Directory,
-}
-
-#[derive(Clone, PartialEq, Debug)]
-struct FileCommandOutput {
-    completion_text: String,
-    files: Vec<OutputFile>,
-}
-
-#[derive(Clone, PartialEq, Debug)]
-struct OutputFile {
-    range_in_text: Range<usize>,
-    path: PathBuf,
-    entry_type: EntryType,
-}
-
 fn collect_files(
     project: Model<Project>,
     glob_inputs: &[String],
     cx: &mut AppContext,
-) -> Task<Result<FileCommandOutput>> {
+) -> Task<Result<SlashCommandOutput>> {
     let Ok(matchers) = glob_inputs
         .into_iter()
         .map(|glob_input| {
@@ -254,8 +218,7 @@ fn collect_files(
         .collect::<Vec<_>>();
 
     cx.spawn(|mut cx| async move {
-        let mut text = String::new();
-        let mut ranges = Vec::new();
+        let mut output = SlashCommandOutput::default();
         for snapshot in snapshots {
             let worktree_id = snapshot.id();
             let mut directory_stack: Vec<(Arc<Path>, String, usize)> = Vec::new();
@@ -279,11 +242,12 @@ fn collect_files(
                         break;
                     }
                     let (_, entry_name, start) = directory_stack.pop().unwrap();
-                    ranges.push(OutputFile {
-                        range_in_text: start..text.len().saturating_sub(1),
-                        path: PathBuf::from(entry_name),
-                        entry_type: EntryType::Directory,
-                    });
+                    output.sections.push(build_entry_output_section(
+                        start..output.text.len().saturating_sub(1),
+                        Some(&PathBuf::from(entry_name)),
+                        true,
+                        None,
+                    ));
                 }
 
                 let filename = entry
@@ -315,21 +279,23 @@ fn collect_files(
                         continue;
                     }
                     let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/");
-                    let entry_start = text.len();
+                    let entry_start = output.text.len();
                     if prefix_paths.is_empty() {
                         if is_top_level_directory {
-                            text.push_str(&path_including_worktree_name.to_string_lossy());
+                            output
+                                .text
+                                .push_str(&path_including_worktree_name.to_string_lossy());
                             is_top_level_directory = false;
                         } else {
-                            text.push_str(&filename);
+                            output.text.push_str(&filename);
                         }
                         directory_stack.push((entry.path.clone(), filename, entry_start));
                     } else {
                         let entry_name = format!("{}/{}", prefix_paths, &filename);
-                        text.push_str(&entry_name);
+                        output.text.push_str(&entry_name);
                         directory_stack.push((entry.path.clone(), entry_name, entry_start));
                     }
-                    text.push('\n');
+                    output.text.push('\n');
                 } else if entry.is_file() {
                     let Some(open_buffer_task) = project_handle
                         .update(&mut cx, |project, cx| {
@@ -340,28 +306,13 @@ fn collect_files(
                         continue;
                     };
                     if let Some(buffer) = open_buffer_task.await.log_err() {
-                        let buffer_snapshot =
-                            cx.read_model(&buffer, |buffer, _| buffer.snapshot())?;
-                        let prev_len = text.len();
-                        collect_file_content(
-                            &mut text,
-                            &buffer_snapshot,
-                            path_including_worktree_name.to_string_lossy().to_string(),
-                        );
-                        text.push('\n');
-                        if !write_single_file_diagnostics(
-                            &mut text,
+                        let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?;
+                        append_buffer_to_output(
+                            &snapshot,
                             Some(&path_including_worktree_name),
-                            &buffer_snapshot,
-                        ) {
-                            text.pop();
-                        }
-                        ranges.push(OutputFile {
-                            range_in_text: prev_len..text.len(),
-                            path: path_including_worktree_name,
-                            entry_type: EntryType::File,
-                        });
-                        text.push('\n');
+                            &mut output,
+                        )
+                        .log_err();
                     }
                 }
             }
@@ -371,42 +322,26 @@ fn collect_files(
                     let mut root_path = PathBuf::new();
                     root_path.push(snapshot.root_name());
                     root_path.push(&dir);
-                    ranges.push(OutputFile {
-                        range_in_text: start..text.len(),
-                        path: root_path,
-                        entry_type: EntryType::Directory,
-                    });
+                    output.sections.push(build_entry_output_section(
+                        start..output.text.len(),
+                        Some(&root_path),
+                        true,
+                        None,
+                    ));
                 } else {
-                    ranges.push(OutputFile {
-                        range_in_text: start..text.len(),
-                        path: PathBuf::from(entry.as_str()),
-                        entry_type: EntryType::Directory,
-                    });
+                    output.sections.push(build_entry_output_section(
+                        start..output.text.len(),
+                        Some(&PathBuf::from(entry.as_str())),
+                        true,
+                        None,
+                    ));
                 }
             }
         }
-        Ok(FileCommandOutput {
-            completion_text: text,
-            files: ranges,
-        })
+        Ok(output)
     })
 }
 
-fn collect_file_content(buffer: &mut String, snapshot: &BufferSnapshot, filename: String) {
-    let mut content = snapshot.text();
-    LineEnding::normalize(&mut content);
-    buffer.reserve(filename.len() + content.len() + 9);
-    buffer.push_str(&codeblock_fence_for_path(
-        Some(&PathBuf::from(filename)),
-        None,
-    ));
-    buffer.push_str(&content);
-    if !buffer.ends_with('\n') {
-        buffer.push('\n');
-    }
-    buffer.push_str("```");
-}
-
 pub fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<Range<u32>>) -> String {
     let mut text = String::new();
     write!(text, "```").unwrap();
@@ -429,6 +364,11 @@ pub fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<Range<u32
     text
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct FileCommandMetadata {
+    pub path: String,
+}
+
 pub fn build_entry_output_section(
     range: Range<usize>,
     path: Option<&Path>,
@@ -454,6 +394,16 @@ pub fn build_entry_output_section(
         range,
         icon,
         label: label.into(),
+        metadata: if is_directory {
+            None
+        } else {
+            path.and_then(|path| {
+                serde_json::to_value(FileCommandMetadata {
+                    path: path.to_string_lossy().to_string(),
+                })
+                .ok()
+            })
+        },
     }
 }
 
@@ -539,6 +489,36 @@ mod custom_path_matcher {
     }
 }
 
+pub fn append_buffer_to_output(
+    buffer: &BufferSnapshot,
+    path: Option<&Path>,
+    output: &mut SlashCommandOutput,
+) -> Result<()> {
+    let prev_len = output.text.len();
+
+    let mut content = buffer.text();
+    LineEnding::normalize(&mut content);
+    output.text.push_str(&codeblock_fence_for_path(path, None));
+    output.text.push_str(&content);
+    if !output.text.ends_with('\n') {
+        output.text.push('\n');
+    }
+    output.text.push_str("```");
+    output.text.push('\n');
+
+    let section_ix = output.sections.len();
+    collect_buffer_diagnostics(output, buffer, false);
+
+    output.sections.insert(
+        section_ix,
+        build_entry_output_section(prev_len..output.text.len(), path, false, None),
+    );
+
+    output.text.push('\n');
+
+    Ok(())
+}
+
 #[cfg(test)]
 mod test {
     use fs::FakeFs;
@@ -591,9 +571,9 @@ mod test {
             .await
             .unwrap();
 
-        assert!(result_1.completion_text.starts_with("root/dir"));
+        assert!(result_1.text.starts_with("root/dir"));
         // 4 files + 2 directories
-        assert_eq!(6, result_1.files.len());
+        assert_eq!(result_1.sections.len(), 6);
 
         let result_2 = cx
             .update(|cx| collect_files(project.clone(), &["root/dir/".to_string()], cx))
@@ -607,9 +587,9 @@ mod test {
             .await
             .unwrap();
 
-        assert!(result.completion_text.starts_with("root/dir"));
+        assert!(result.text.starts_with("root/dir"));
         // 5 files + 2 directories
-        assert_eq!(7, result.files.len());
+        assert_eq!(result.sections.len(), 7);
 
         // Ensure that the project lasts until after the last await
         drop(project);
@@ -654,36 +634,27 @@ mod test {
             .unwrap();
 
         // Sanity check
-        assert!(result.completion_text.starts_with("zed/assets/themes\n"));
-        assert_eq!(7, result.files.len());
+        assert!(result.text.starts_with("zed/assets/themes\n"));
+        assert_eq!(result.sections.len(), 7);
 
         // Ensure that full file paths are included in the real output
-        assert!(result
-            .completion_text
-            .contains("zed/assets/themes/andromeda/LICENSE"));
-        assert!(result
-            .completion_text
-            .contains("zed/assets/themes/ayu/LICENSE"));
-        assert!(result
-            .completion_text
-            .contains("zed/assets/themes/summercamp/LICENSE"));
-
-        assert_eq!("summercamp", result.files[5].path.to_string_lossy());
+        assert!(result.text.contains("zed/assets/themes/andromeda/LICENSE"));
+        assert!(result.text.contains("zed/assets/themes/ayu/LICENSE"));
+        assert!(result.text.contains("zed/assets/themes/summercamp/LICENSE"));
+
+        assert_eq!(result.sections[5].label, "summercamp");
 
         // Ensure that things are in descending order, with properly relativized paths
         assert_eq!(
-            "zed/assets/themes/andromeda/LICENSE",
-            result.files[0].path.to_string_lossy()
-        );
-        assert_eq!("andromeda", result.files[1].path.to_string_lossy());
-        assert_eq!(
-            "zed/assets/themes/ayu/LICENSE",
-            result.files[2].path.to_string_lossy()
+            result.sections[0].label,
+            "zed/assets/themes/andromeda/LICENSE"
         );
-        assert_eq!("ayu", result.files[3].path.to_string_lossy());
+        assert_eq!(result.sections[1].label, "andromeda");
+        assert_eq!(result.sections[2].label, "zed/assets/themes/ayu/LICENSE");
+        assert_eq!(result.sections[3].label, "ayu");
         assert_eq!(
-            "zed/assets/themes/summercamp/LICENSE",
-            result.files[4].path.to_string_lossy()
+            result.sections[4].label,
+            "zed/assets/themes/summercamp/LICENSE"
         );
 
         // Ensure that the project lasts until after the last await
@@ -723,27 +694,24 @@ mod test {
             .await
             .unwrap();
 
-        assert!(result.completion_text.starts_with("zed/assets/themes\n"));
-        assert_eq!(
-            "zed/assets/themes/LICENSE",
-            result.files[0].path.to_string_lossy()
-        );
+        assert!(result.text.starts_with("zed/assets/themes\n"));
+        assert_eq!(result.sections[0].label, "zed/assets/themes/LICENSE");
         assert_eq!(
-            "zed/assets/themes/summercamp/LICENSE",
-            result.files[1].path.to_string_lossy()
+            result.sections[1].label,
+            "zed/assets/themes/summercamp/LICENSE"
         );
         assert_eq!(
-            "zed/assets/themes/summercamp/subdir/LICENSE",
-            result.files[2].path.to_string_lossy()
+            result.sections[2].label,
+            "zed/assets/themes/summercamp/subdir/LICENSE"
         );
         assert_eq!(
-            "zed/assets/themes/summercamp/subdir/subsubdir/LICENSE",
-            result.files[3].path.to_string_lossy()
+            result.sections[3].label,
+            "zed/assets/themes/summercamp/subdir/subsubdir/LICENSE"
         );
-        assert_eq!("subsubdir", result.files[4].path.to_string_lossy());
-        assert_eq!("subdir", result.files[5].path.to_string_lossy());
-        assert_eq!("summercamp", result.files[6].path.to_string_lossy());
-        assert_eq!("zed/assets/themes", result.files[7].path.to_string_lossy());
+        assert_eq!(result.sections[4].label, "subsubdir");
+        assert_eq!(result.sections[5].label, "subdir");
+        assert_eq!(result.sections[6].label, "summercamp");
+        assert_eq!(result.sections[7].label, "zed/assets/themes");
 
         // Ensure that the project lasts until after the last await
         drop(project);

crates/assistant/src/slash_command/now_command.rs 🔗

@@ -7,7 +7,7 @@ use assistant_slash_command::{
 };
 use chrono::Local;
 use gpui::{Task, WeakView};
-use language::LspAdapterDelegate;
+use language::{BufferSnapshot, LspAdapterDelegate};
 use ui::prelude::*;
 use workspace::Workspace;
 
@@ -43,6 +43,8 @@ impl SlashCommand for NowSlashCommand {
     fn run(
         self: Arc<Self>,
         _arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         _workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         _cx: &mut WindowContext,
@@ -57,6 +59,7 @@ impl SlashCommand for NowSlashCommand {
                 range,
                 icon: IconName::CountdownTimer,
                 label: now.to_rfc2822().into(),
+                metadata: None,
             }],
             run_commands_in_text: false,
         }))

crates/assistant/src/slash_command/project_command.rs 🔗

@@ -3,7 +3,7 @@ use anyhow::{anyhow, Context, Result};
 use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
 use fs::Fs;
 use gpui::{AppContext, Model, Task, WeakView};
-use language::LspAdapterDelegate;
+use language::{BufferSnapshot, LspAdapterDelegate};
 use project::{Project, ProjectPath};
 use std::{
     fmt::Write,
@@ -118,6 +118,8 @@ impl SlashCommand for ProjectSlashCommand {
     fn run(
         self: Arc<Self>,
         _arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -140,6 +142,7 @@ impl SlashCommand for ProjectSlashCommand {
                         range,
                         icon: IconName::FileTree,
                         label: "Project".into(),
+                        metadata: None,
                     }],
                     run_commands_in_text: false,
                 })

crates/assistant/src/slash_command/prompt_command.rs 🔗

@@ -3,7 +3,7 @@ use crate::prompt_library::PromptStore;
 use anyhow::{anyhow, Context, Result};
 use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
 use gpui::{Task, WeakView};
-use language::LspAdapterDelegate;
+use language::{BufferSnapshot, LspAdapterDelegate};
 use std::sync::{atomic::AtomicBool, Arc};
 use ui::prelude::*;
 use workspace::Workspace;
@@ -56,6 +56,8 @@ impl SlashCommand for PromptSlashCommand {
     fn run(
         self: Arc<Self>,
         arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         _workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -95,6 +97,7 @@ impl SlashCommand for PromptSlashCommand {
                     range,
                     icon: IconName::Library,
                     label: title,
+                    metadata: None,
                 }],
                 run_commands_in_text: true,
             })

crates/assistant/src/slash_command/search_command.rs 🔗

@@ -60,6 +60,8 @@ impl SlashCommand for SearchSlashCommand {
     fn run(
         self: Arc<Self>,
         arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: language::BufferSnapshot,
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -168,6 +170,7 @@ impl SlashCommand for SearchSlashCommand {
                         range: 0..text.len(),
                         icon: IconName::MagnifyingGlass,
                         label: query,
+                        metadata: None,
                     });
 
                     SlashCommandOutput {

crates/assistant/src/slash_command/symbols_command.rs 🔗

@@ -3,7 +3,7 @@ use anyhow::{anyhow, Context as _, Result};
 use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
 use editor::Editor;
 use gpui::{Task, WeakView};
-use language::LspAdapterDelegate;
+use language::{BufferSnapshot, LspAdapterDelegate};
 use std::sync::Arc;
 use std::{path::Path, sync::atomic::AtomicBool};
 use ui::{IconName, WindowContext};
@@ -41,6 +41,8 @@ impl SlashCommand for OutlineSlashCommand {
     fn run(
         self: Arc<Self>,
         _arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -77,6 +79,7 @@ impl SlashCommand for OutlineSlashCommand {
                         range: 0..outline_text.len(),
                         icon: IconName::ListTree,
                         label: path.to_string_lossy().to_string().into(),
+                        metadata: None,
                     }],
                     text: outline_text,
                     run_commands_in_text: false,

crates/assistant/src/slash_command/tab_command.rs 🔗

@@ -1,21 +1,17 @@
-use super::{
-    diagnostics_command::write_single_file_diagnostics,
-    file_command::{build_entry_output_section, codeblock_fence_for_path},
-    SlashCommand, SlashCommandOutput,
-};
+use super::{file_command::append_buffer_to_output, SlashCommand, SlashCommandOutput};
 use anyhow::{Context, Result};
-use assistant_slash_command::ArgumentCompletion;
+use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
 use collections::{HashMap, HashSet};
 use editor::Editor;
 use futures::future::join_all;
 use gpui::{Entity, Task, WeakView};
 use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
 use std::{
-    fmt::Write,
     path::PathBuf,
     sync::{atomic::AtomicBool, Arc},
 };
 use ui::{ActiveTheme, WindowContext};
+use util::ResultExt;
 use workspace::Workspace;
 
 pub(crate) struct TabSlashCommand;
@@ -131,6 +127,8 @@ impl SlashCommand for TabSlashCommand {
     fn run(
         self: Arc<Self>,
         arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -144,40 +142,11 @@ impl SlashCommand for TabSlashCommand {
         );
 
         cx.background_executor().spawn(async move {
-            let mut sections = Vec::new();
-            let mut text = String::new();
-            let mut has_diagnostics = false;
+            let mut output = SlashCommandOutput::default();
             for (full_path, buffer, _) in tab_items_search.await? {
-                let section_start_ix = text.len();
-                text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
-                for chunk in buffer.as_rope().chunks() {
-                    text.push_str(chunk);
-                }
-                if !text.ends_with('\n') {
-                    text.push('\n');
-                }
-                writeln!(text, "```").unwrap();
-                if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
-                    has_diagnostics = true;
-                }
-                if !text.ends_with('\n') {
-                    text.push('\n');
-                }
-
-                let section_end_ix = text.len() - 1;
-                sections.push(build_entry_output_section(
-                    section_start_ix..section_end_ix,
-                    full_path.as_deref(),
-                    false,
-                    None,
-                ));
+                append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err();
             }
-
-            Ok(SlashCommandOutput {
-                text,
-                sections,
-                run_commands_in_text: has_diagnostics,
-            })
+            Ok(output)
         })
     }
 }

crates/assistant/src/slash_command/terminal_command.rs 🔗

@@ -6,7 +6,7 @@ use assistant_slash_command::{
     ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
 };
 use gpui::{AppContext, Task, View, WeakView};
-use language::{CodeLabel, LspAdapterDelegate};
+use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
 use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
 use ui::prelude::*;
 use workspace::{dock::Panel, Workspace};
@@ -57,6 +57,8 @@ impl SlashCommand for TerminalSlashCommand {
     fn run(
         self: Arc<Self>,
         arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -91,6 +93,7 @@ impl SlashCommand for TerminalSlashCommand {
                 range,
                 icon: IconName::Terminal,
                 label: "Terminal".into(),
+                metadata: None,
             }],
             run_commands_in_text: false,
         }))

crates/assistant/src/slash_command/workflow_command.rs 🔗

@@ -8,7 +8,7 @@ use assistant_slash_command::{
     ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
 };
 use gpui::{Task, WeakView};
-use language::LspAdapterDelegate;
+use language::{BufferSnapshot, LspAdapterDelegate};
 use ui::prelude::*;
 
 use workspace::Workspace;
@@ -53,6 +53,8 @@ impl SlashCommand for WorkflowSlashCommand {
     fn run(
         self: Arc<Self>,
         _arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         _workspace: WeakView<Workspace>,
         _delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -68,6 +70,7 @@ impl SlashCommand for WorkflowSlashCommand {
                     range,
                     icon: IconName::Route,
                     label: "Workflow".into(),
+                    metadata: None,
                 }],
                 run_commands_in_text: false,
             })

crates/assistant_slash_command/src/assistant_slash_command.rs 🔗

@@ -2,7 +2,7 @@ mod slash_command_registry;
 
 use anyhow::Result;
 use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
-use language::{CodeLabel, LspAdapterDelegate};
+use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
 use serde::{Deserialize, Serialize};
 pub use slash_command_registry::*;
 use std::{
@@ -77,6 +77,8 @@ pub trait SlashCommand: 'static + Send + Sync {
     fn run(
         self: Arc<Self>,
         arguments: &[String],
+        context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        context_buffer: BufferSnapshot,
         workspace: WeakView<Workspace>,
         // TODO: We're just using the `LspAdapterDelegate` here because that is
         // what the extension API is already expecting.
@@ -94,7 +96,7 @@ pub type RenderFoldPlaceholder = Arc<
         + Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
 >;
 
-#[derive(Debug, Default)]
+#[derive(Debug, Default, PartialEq)]
 pub struct SlashCommandOutput {
     pub text: String,
     pub sections: Vec<SlashCommandOutputSection<usize>>,
@@ -106,4 +108,11 @@ pub struct SlashCommandOutputSection<T> {
     pub range: Range<T>,
     pub icon: IconName,
     pub label: SharedString,
+    pub metadata: Option<serde_json::Value>,
+}
+
+impl SlashCommandOutputSection<language::Anchor> {
+    pub fn is_valid(&self, buffer: &language::TextBuffer) -> bool {
+        self.range.start.is_valid(buffer) && !self.range.to_offset(buffer).is_empty()
+    }
 }

crates/extension/src/extension_slash_command.rs 🔗

@@ -6,7 +6,7 @@ use assistant_slash_command::{
 };
 use futures::FutureExt;
 use gpui::{Task, WeakView, WindowContext};
-use language::LspAdapterDelegate;
+use language::{BufferSnapshot, LspAdapterDelegate};
 use ui::prelude::*;
 use wasmtime_wasi::WasiView;
 use workspace::Workspace;
@@ -82,6 +82,8 @@ impl SlashCommand for ExtensionSlashCommand {
     fn run(
         self: Arc<Self>,
         arguments: &[String],
+        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
+        _context_buffer: BufferSnapshot,
         _workspace: WeakView<Workspace>,
         delegate: Option<Arc<dyn LspAdapterDelegate>>,
         cx: &mut WindowContext,
@@ -121,6 +123,7 @@ impl SlashCommand for ExtensionSlashCommand {
                         range: section.range.into(),
                         icon: IconName::Code,
                         label: section.label.into(),
+                        metadata: None,
                     })
                     .collect(),
                 run_commands_in_text: false,

crates/proto/proto/zed.proto 🔗

@@ -2390,6 +2390,7 @@ message SlashCommandOutputSection {
     AnchorRange range = 1;
     string icon_name = 2;
     string label = 3;
+    optional string metadata = 4;
 }
 
 message ContextOperation {