diff --git a/Cargo.lock b/Cargo.lock index b31beee09c..6d0f7f54a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,6 +455,7 @@ dependencies = [ "language", "parking_lot", "serde", + "serde_json", "workspace", ] diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 7a73c188ec..af7f03ebb3 100644 --- a/crates/assistant/src/assistant.rs +++ b/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>, 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); diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 59f5e81d05..52838b5c77 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1906,7 +1906,22 @@ impl ContextEditor { cx: &mut ViewContext, ) { 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::>(); + let snapshot = context.buffer().read(cx).snapshot(); + let output = command.run( + arguments, + §ions, + snapshot, + workspace, + self.lsp_adapter_delegate.clone(), + cx, + ); self.context.update(cx, |context, cx| { context.insert_command_output( command_range, diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 38ccddb962..d55b1aee08 100644 --- a/crates/assistant/src/context.rs +++ b/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::>>()?, @@ -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::>(); 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(), diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index c851ca7438..842ac05078 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/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, _arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, _workspace: WeakView, _delegate: Option>, _cx: &mut WindowContext, diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index 387e8231e4..cf957a15c6 100644 --- a/crates/assistant/src/slash_command.rs +++ b/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; diff --git a/crates/assistant/src/slash_command/auto_command.rs b/crates/assistant/src/slash_command/auto_command.rs index cedfc63702..e1f20c311b 100644 --- a/crates/assistant/src/slash_command/auto_command.rs +++ b/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, arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: language::BufferSnapshot, workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index 8ae9430a99..6b1ae39186 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/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, arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, _workspace: WeakView, _delegate: Option>, 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, diff --git a/crates/assistant/src/slash_command/default_command.rs b/crates/assistant/src/slash_command/default_command.rs index 18db87b322..4199840300 100644 --- a/crates/assistant/src/slash_command/default_command.rs +++ b/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, _arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, _workspace: WeakView, _delegate: Option>, 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, diff --git a/crates/assistant/src/slash_command/delta_command.rs b/crates/assistant/src/slash_command/delta_command.rs new file mode 100644 index 0000000000..6a66ad3f09 --- /dev/null +++ b/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, + _arguments: &[String], + _cancellation_flag: Arc, + _workspace: Option>, + _cx: &mut WindowContext, + ) -> Task>> { + unimplemented!() + } + + fn run( + self: Arc, + _arguments: &[String], + context_slash_command_output_sections: &[SlashCommandOutputSection], + context_buffer: BufferSnapshot, + workspace: WeakView, + delegate: Option>, + cx: &mut WindowContext, + ) -> Task> { + 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::(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) + }) + } +} diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index 2105830651..3f79c01675 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/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, arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, workspace: WeakView, _delegate: Option>, 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, options: Options, cx: &mut AppContext, -) -> Task, PlaceholderType)>)>>> { +) -> Task>> { 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, 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))) + let mut label = String::new(); + label.push_str("Diagnostics"); + if let Some(source) = error_source { + write!(label, " ({})", source).unwrap(); + } + + 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(); + } + } + + output.sections.insert( + 0, + SlashCommandOutputSection { + range: 0..output.text.len(), + icon: IconName::Warning, + label: label.into(), + metadata: None, + }, + ); + + Ok(Some(output)) }) } -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; - } - } - false -} - -pub fn write_single_file_diagnostics( - output: &mut String, - path: Option<&Path>, +pub fn collect_buffer_diagnostics( + output: &mut SlashCommandOutput, 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; - } - } - false -} - -fn collect_buffer_diagnostics( - text: &mut String, - sections: &mut Vec<(Range, PlaceholderType)>, - 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, PlaceholderType)>, + output: &mut SlashCommandOutput, entry: &DiagnosticEntry, 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), - 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, + }); } diff --git a/crates/assistant/src/slash_command/docs_command.rs b/crates/assistant/src/slash_command/docs_command.rs index e114cfeab7..399ede9d99 100644 --- a/crates/assistant/src/slash_command/docs_command.rs +++ b/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, arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, _workspace: WeakView, _delegate: Option>, 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, diff --git a/crates/assistant/src/slash_command/fetch_command.rs b/crates/assistant/src/slash_command/fetch_command.rs index 8ecb6de759..23d3c884a8 100644 --- a/crates/assistant/src/slash_command/fetch_command.rs +++ b/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, arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, workspace: WeakView, _delegate: Option>, 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, }) diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index e5d8f1b2d6..0df8b5d4e0 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/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, arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, workspace: WeakView, _delegate: Option>, 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, -} - -#[derive(Clone, PartialEq, Debug)] -struct OutputFile { - range_in_text: Range, - path: PathBuf, - entry_type: EntryType, -} - fn collect_files( project: Model, glob_inputs: &[String], cx: &mut AppContext, -) -> Task> { +) -> Task> { let Ok(matchers) = glob_inputs .into_iter() .map(|glob_input| { @@ -254,8 +218,7 @@ fn collect_files( .collect::>(); 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, 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>) -> 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, 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!(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!("summercamp", result.files[5].path.to_string_lossy()); + 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() + result.sections[0].label, + "zed/assets/themes/andromeda/LICENSE" ); - assert_eq!("andromeda", result.files[1].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/ayu/LICENSE", - result.files[2].path.to_string_lossy() - ); - assert_eq!("ayu", result.files[3].path.to_string_lossy()); - 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!(result.text.starts_with("zed/assets/themes\n")); + assert_eq!(result.sections[0].label, "zed/assets/themes/LICENSE"); assert_eq!( - "zed/assets/themes/LICENSE", - result.files[0].path.to_string_lossy() + result.sections[1].label, + "zed/assets/themes/summercamp/LICENSE" ); assert_eq!( - "zed/assets/themes/summercamp/LICENSE", - result.files[1].path.to_string_lossy() + result.sections[2].label, + "zed/assets/themes/summercamp/subdir/LICENSE" ); assert_eq!( - "zed/assets/themes/summercamp/subdir/LICENSE", - result.files[2].path.to_string_lossy() + result.sections[3].label, + "zed/assets/themes/summercamp/subdir/subsubdir/LICENSE" ); - assert_eq!( - "zed/assets/themes/summercamp/subdir/subsubdir/LICENSE", - result.files[3].path.to_string_lossy() - ); - 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); diff --git a/crates/assistant/src/slash_command/now_command.rs b/crates/assistant/src/slash_command/now_command.rs index eb6277a7d9..eb0ca926f0 100644 --- a/crates/assistant/src/slash_command/now_command.rs +++ b/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, _arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, _workspace: WeakView, _delegate: Option>, _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, })) diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index 8182734e72..3e8596d942 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/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, _arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, workspace: WeakView, _delegate: Option>, 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, }) diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index 4d64bba2ed..effbcc0f90 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/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, arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, _workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, @@ -95,6 +97,7 @@ impl SlashCommand for PromptSlashCommand { range, icon: IconName::Library, label: title, + metadata: None, }], run_commands_in_text: true, }) diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index 3a513ed9ad..72d86ec5c5 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/crates/assistant/src/slash_command/search_command.rs @@ -60,6 +60,8 @@ impl SlashCommand for SearchSlashCommand { fn run( self: Arc, arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: language::BufferSnapshot, workspace: WeakView, _delegate: Option>, cx: &mut WindowContext, @@ -168,6 +170,7 @@ impl SlashCommand for SearchSlashCommand { range: 0..text.len(), icon: IconName::MagnifyingGlass, label: query, + metadata: None, }); SlashCommandOutput { diff --git a/crates/assistant/src/slash_command/symbols_command.rs b/crates/assistant/src/slash_command/symbols_command.rs index c9582f2882..1cf8536c0d 100644 --- a/crates/assistant/src/slash_command/symbols_command.rs +++ b/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, _arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, workspace: WeakView, _delegate: Option>, 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, diff --git a/crates/assistant/src/slash_command/tab_command.rs b/crates/assistant/src/slash_command/tab_command.rs index 1a6884b853..bdf8450d43 100644 --- a/crates/assistant/src/slash_command/tab_command.rs +++ b/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, arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, workspace: WeakView, _delegate: Option>, 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) }) } } diff --git a/crates/assistant/src/slash_command/terminal_command.rs b/crates/assistant/src/slash_command/terminal_command.rs index 04baabd396..1d0293c235 100644 --- a/crates/assistant/src/slash_command/terminal_command.rs +++ b/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, arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, workspace: WeakView, _delegate: Option>, 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, })) diff --git a/crates/assistant/src/slash_command/workflow_command.rs b/crates/assistant/src/slash_command/workflow_command.rs index f588fe848d..c66dd9bebf 100644 --- a/crates/assistant/src/slash_command/workflow_command.rs +++ b/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, _arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, _workspace: WeakView, _delegate: Option>, 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, }) diff --git a/crates/assistant_slash_command/Cargo.toml b/crates/assistant_slash_command/Cargo.toml index 3d764bb0be..a58a84312f 100644 --- a/crates/assistant_slash_command/Cargo.toml +++ b/crates/assistant_slash_command/Cargo.toml @@ -19,4 +19,5 @@ gpui.workspace = true language.workspace = true parking_lot.workspace = true serde.workspace = true +serde_json.workspace = true workspace.workspace = true diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index c5dece11ca..36e229d49a 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/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, arguments: &[String], + context_slash_command_output_sections: &[SlashCommandOutputSection], + context_buffer: BufferSnapshot, workspace: WeakView, // 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, &mut WindowContext) -> AnyElement, >; -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq)] pub struct SlashCommandOutput { pub text: String, pub sections: Vec>, @@ -106,4 +108,11 @@ pub struct SlashCommandOutputSection { pub range: Range, pub icon: IconName, pub label: SharedString, + pub metadata: Option, +} + +impl SlashCommandOutputSection { + pub fn is_valid(&self, buffer: &language::TextBuffer) -> bool { + self.range.start.is_valid(buffer) && !self.range.to_offset(buffer).is_empty() + } } diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index 60b027ef9d..3dfbc4c03d 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/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, arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, _workspace: WeakView, delegate: Option>, 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, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index a10b3798a4..77942c8a94 100644 --- a/crates/proto/proto/zed.proto +++ b/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 {