Introduce a new /delta
command (#17903)
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>
This commit is contained in:
parent
a20c0eb626
commit
54b8232be2
26 changed files with 408 additions and 366 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -455,6 +455,7 @@ dependencies = [
|
||||||
"language",
|
"language",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -41,9 +41,9 @@ use semantic_index::{CloudEmbeddingProvider, SemanticDb};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{update_settings_file, Settings, SettingsStore};
|
use settings::{update_settings_file, Settings, SettingsStore};
|
||||||
use slash_command::{
|
use slash_command::{
|
||||||
auto_command, context_server_command, default_command, diagnostics_command, docs_command,
|
auto_command, context_server_command, default_command, delta_command, diagnostics_command,
|
||||||
fetch_command, file_command, now_command, project_command, prompt_command, search_command,
|
docs_command, fetch_command, file_command, now_command, project_command, prompt_command,
|
||||||
symbols_command, tab_command, terminal_command, workflow_command,
|
search_command, symbols_command, tab_command, terminal_command, workflow_command,
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
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);
|
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||||
|
|
||||||
slash_command_registry.register_command(file_command::FileSlashCommand, true);
|
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(symbols_command::OutlineSlashCommand, true);
|
||||||
slash_command_registry.register_command(tab_command::TabSlashCommand, true);
|
slash_command_registry.register_command(tab_command::TabSlashCommand, true);
|
||||||
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
|
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
|
||||||
|
|
|
@ -1906,7 +1906,22 @@ impl ContextEditor {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
|
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,
|
||||||
|
§ions,
|
||||||
|
snapshot,
|
||||||
|
workspace,
|
||||||
|
self.lsp_adapter_delegate.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
self.context.update(cx, |context, cx| {
|
self.context.update(cx, |context, cx| {
|
||||||
context.insert_command_output(
|
context.insert_command_output(
|
||||||
command_range,
|
command_range,
|
||||||
|
|
|
@ -48,7 +48,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use telemetry_events::AssistantKind;
|
use telemetry_events::AssistantKind;
|
||||||
use text::BufferSnapshot;
|
use text::BufferSnapshot;
|
||||||
use util::{post_inc, TryFutureExt};
|
use util::{post_inc, ResultExt, TryFutureExt};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
|
@ -162,6 +162,9 @@ impl ContextOperation {
|
||||||
)?,
|
)?,
|
||||||
icon: section.icon_name.parse()?,
|
icon: section.icon_name.parse()?,
|
||||||
label: section.label.into(),
|
label: section.label.into(),
|
||||||
|
metadata: section
|
||||||
|
.metadata
|
||||||
|
.and_then(|metadata| serde_json::from_str(&metadata).log_err()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>>>()?,
|
.collect::<Result<Vec<_>>>()?,
|
||||||
|
@ -242,6 +245,9 @@ impl ContextOperation {
|
||||||
)),
|
)),
|
||||||
icon_name: icon_name.to_string(),
|
icon_name: icon_name.to_string(),
|
||||||
label: section.label.to_string(),
|
label: section.label.to_string(),
|
||||||
|
metadata: section.metadata.as_ref().and_then(|metadata| {
|
||||||
|
serde_json::to_string(metadata).log_err()
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -635,12 +641,13 @@ impl Context {
|
||||||
.slash_command_output_sections
|
.slash_command_output_sections
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|section| {
|
.filter_map(|section| {
|
||||||
let range = section.range.to_offset(buffer);
|
if section.is_valid(buffer) {
|
||||||
if section.range.start.is_valid(buffer) && !range.is_empty() {
|
let range = section.range.to_offset(buffer);
|
||||||
Some(assistant_slash_command::SlashCommandOutputSection {
|
Some(assistant_slash_command::SlashCommandOutputSection {
|
||||||
range,
|
range,
|
||||||
icon: section.icon,
|
icon: section.icon,
|
||||||
label: section.label.clone(),
|
label: section.label.clone(),
|
||||||
|
metadata: section.metadata.clone(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -1825,6 +1832,7 @@ impl Context {
|
||||||
..buffer.anchor_before(start + section.range.end),
|
..buffer.anchor_before(start + section.range.end),
|
||||||
icon: section.icon,
|
icon: section.icon,
|
||||||
label: section.label,
|
label: section.label,
|
||||||
|
metadata: section.metadata,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
||||||
|
@ -2977,6 +2985,7 @@ impl SavedContext {
|
||||||
..buffer.anchor_before(section.range.end),
|
..buffer.anchor_before(section.range.end),
|
||||||
icon: section.icon,
|
icon: section.icon,
|
||||||
label: section.label,
|
label: section.label,
|
||||||
|
metadata: section.metadata,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
|
@ -12,7 +12,7 @@ use assistant_slash_command::{
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
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 language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
@ -1089,6 +1089,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||||
range: section_start..section_end,
|
range: section_start..section_end,
|
||||||
icon: ui::IconName::Ai,
|
icon: ui::IconName::Ai,
|
||||||
label: "section".into(),
|
label: "section".into(),
|
||||||
|
metadata: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1425,6 +1426,8 @@ impl SlashCommand for FakeSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
_arguments: &[String],
|
_arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
_cx: &mut WindowContext,
|
_cx: &mut WindowContext,
|
||||||
|
|
|
@ -22,6 +22,7 @@ use workspace::Workspace;
|
||||||
pub mod auto_command;
|
pub mod auto_command;
|
||||||
pub mod context_server_command;
|
pub mod context_server_command;
|
||||||
pub mod default_command;
|
pub mod default_command;
|
||||||
|
pub mod delta_command;
|
||||||
pub mod diagnostics_command;
|
pub mod diagnostics_command;
|
||||||
pub mod docs_command;
|
pub mod docs_command;
|
||||||
pub mod fetch_command;
|
pub mod fetch_command;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use super::create_label_for_command;
|
use super::create_label_for_command;
|
||||||
use super::{SlashCommand, SlashCommandOutput};
|
use super::{SlashCommand, SlashCommandOutput};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_slash_command::ArgumentCompletion;
|
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||||
use feature_flags::FeatureFlag;
|
use feature_flags::FeatureFlag;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{AppContext, AsyncAppContext, Task, WeakView};
|
use gpui::{AppContext, AsyncAppContext, Task, WeakView};
|
||||||
|
@ -87,6 +87,8 @@ impl SlashCommand for AutoCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: language::BufferSnapshot,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
|
|
@ -9,7 +9,7 @@ use context_servers::{
|
||||||
protocol::PromptInfo,
|
protocol::PromptInfo,
|
||||||
};
|
};
|
||||||
use gpui::{Task, WeakView, WindowContext};
|
use gpui::{Task, WeakView, WindowContext};
|
||||||
use language::{CodeLabel, LspAdapterDelegate};
|
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use text::LineEnding;
|
use text::LineEnding;
|
||||||
|
@ -96,7 +96,6 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||||
replace_previous_arguments: false,
|
replace_previous_arguments: false,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(completions)
|
Ok(completions)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -107,6 +106,8 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -141,6 +142,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||||
.description
|
.description
|
||||||
.unwrap_or(format!("Result from {}", prompt_name)),
|
.unwrap_or(format!("Result from {}", prompt_name)),
|
||||||
),
|
),
|
||||||
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
text: prompt,
|
text: prompt,
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::prompt_library::PromptStore;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||||
use gpui::{Task, WeakView};
|
use gpui::{Task, WeakView};
|
||||||
use language::LspAdapterDelegate;
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
|
@ -43,6 +43,8 @@ impl SlashCommand for DefaultSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
_arguments: &[String],
|
_arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -70,6 +72,7 @@ impl SlashCommand for DefaultSlashCommand {
|
||||||
range: 0..text.len(),
|
range: 0..text.len(),
|
||||||
icon: IconName::Library,
|
icon: IconName::Library,
|
||||||
label: "Default".into(),
|
label: "Default".into(),
|
||||||
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
text,
|
text,
|
||||||
run_commands_in_text: true,
|
run_commands_in_text: true,
|
||||||
|
|
109
crates/assistant/src/slash_command/delta_command.rs
Normal file
109
crates/assistant/src/slash_command/delta_command.rs
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,10 +9,9 @@ use language::{
|
||||||
};
|
};
|
||||||
use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
|
use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use std::fmt::Write;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::{
|
use std::{
|
||||||
ops::Range,
|
fmt::Write,
|
||||||
|
path::{Path, PathBuf},
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
};
|
};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
@ -163,6 +162,8 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -175,68 +176,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||||
|
|
||||||
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
|
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
|
||||||
|
|
||||||
cx.spawn(move |_| async move {
|
cx.spawn(move |_| async move { task.await?.ok_or_else(|| anyhow!("No diagnostics found")) })
|
||||||
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,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,7 +217,7 @@ fn collect_diagnostics(
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
options: Options,
|
options: Options,
|
||||||
cx: &mut AppContext,
|
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 {
|
let error_source = if let Some(path_matcher) = &options.path_matcher {
|
||||||
debug_assert_eq!(path_matcher.sources().len(), 1);
|
debug_assert_eq!(path_matcher.sources().len(), 1);
|
||||||
Some(path_matcher.sources().first().cloned().unwrap_or_default())
|
Some(path_matcher.sources().first().cloned().unwrap_or_default())
|
||||||
|
@ -318,13 +258,13 @@ fn collect_diagnostics(
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
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() {
|
if let Some(error_source) = error_source.as_ref() {
|
||||||
writeln!(text, "diagnostics: {}", error_source).unwrap();
|
writeln!(output.text, "diagnostics: {}", error_source).unwrap();
|
||||||
} else {
|
} 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();
|
let mut project_summary = DiagnosticSummary::default();
|
||||||
for (project_path, path, summary) in diagnostic_summaries {
|
for (project_path, path, summary) in diagnostic_summaries {
|
||||||
|
@ -341,10 +281,10 @@ fn collect_diagnostics(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let last_end = text.len();
|
let last_end = output.text.len();
|
||||||
let file_path = path.to_string_lossy().to_string();
|
let file_path = path.to_string_lossy().to_string();
|
||||||
if !glob_is_exact_file_match {
|
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
|
if let Some(buffer) = project_handle
|
||||||
|
@ -352,75 +292,73 @@ fn collect_diagnostics(
|
||||||
.await
|
.await
|
||||||
.log_err()
|
.log_err()
|
||||||
{
|
{
|
||||||
collect_buffer_diagnostics(
|
let snapshot = cx.read_model(&buffer, |buffer, _| buffer.snapshot())?;
|
||||||
&mut text,
|
collect_buffer_diagnostics(&mut output, &snapshot, options.include_warnings);
|
||||||
&mut sections,
|
|
||||||
cx.read_model(&buffer, |buffer, _| buffer.snapshot())?,
|
|
||||||
options.include_warnings,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !glob_is_exact_file_match {
|
if !glob_is_exact_file_match {
|
||||||
sections.push((
|
output.sections.push(SlashCommandOutputSection {
|
||||||
last_end..text.len().saturating_sub(1),
|
range: last_end..output.text.len().saturating_sub(1),
|
||||||
PlaceholderType::File(file_path),
|
icon: IconName::File,
|
||||||
))
|
label: file_path.into(),
|
||||||
|
metadata: None,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No diagnostics found
|
// No diagnostics found
|
||||||
if sections.is_empty() {
|
if output.sections.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
sections.push((
|
let mut label = String::new();
|
||||||
0..text.len(),
|
label.push_str("Diagnostics");
|
||||||
PlaceholderType::Root(project_summary, error_source),
|
if let Some(source) = error_source {
|
||||||
));
|
write!(label, " ({})", source).unwrap();
|
||||||
Ok(Some((text, sections)))
|
}
|
||||||
|
|
||||||
|
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 {
|
pub fn collect_buffer_diagnostics(
|
||||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
output: &mut SlashCommandOutput,
|
||||||
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>,
|
|
||||||
snapshot: &BufferSnapshot,
|
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<usize>, PlaceholderType)>,
|
|
||||||
snapshot: BufferSnapshot,
|
|
||||||
include_warnings: bool,
|
include_warnings: bool,
|
||||||
) {
|
) {
|
||||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||||
let entry = &group.entries[group.primary_ix];
|
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(
|
fn collect_diagnostic(
|
||||||
text: &mut String,
|
output: &mut SlashCommandOutput,
|
||||||
sections: &mut Vec<(Range<usize>, PlaceholderType)>,
|
|
||||||
entry: &DiagnosticEntry<Anchor>,
|
entry: &DiagnosticEntry<Anchor>,
|
||||||
snapshot: &BufferSnapshot,
|
snapshot: &BufferSnapshot,
|
||||||
include_warnings: bool,
|
include_warnings: bool,
|
||||||
|
@ -428,17 +366,17 @@ fn collect_diagnostic(
|
||||||
const EXCERPT_EXPANSION_SIZE: u32 = 2;
|
const EXCERPT_EXPANSION_SIZE: u32 = 2;
|
||||||
const MAX_MESSAGE_LENGTH: usize = 2000;
|
const MAX_MESSAGE_LENGTH: usize = 2000;
|
||||||
|
|
||||||
let ty = match entry.diagnostic.severity {
|
let (ty, icon) = match entry.diagnostic.severity {
|
||||||
DiagnosticSeverity::WARNING => {
|
DiagnosticSeverity::WARNING => {
|
||||||
if !include_warnings {
|
if !include_warnings {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DiagnosticType::Warning
|
("warning", IconName::Warning)
|
||||||
}
|
}
|
||||||
DiagnosticSeverity::ERROR => DiagnosticType::Error,
|
DiagnosticSeverity::ERROR => ("error", IconName::XCircle),
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
let prev_len = text.len();
|
let prev_len = output.text.len();
|
||||||
|
|
||||||
let range = entry.range.to_point(snapshot);
|
let range = entry.range.to_point(snapshot);
|
||||||
let diagnostic_row_number = range.start.row + 1;
|
let diagnostic_row_number = range.start.row + 1;
|
||||||
|
@ -448,11 +386,11 @@ fn collect_diagnostic(
|
||||||
let excerpt_range =
|
let excerpt_range =
|
||||||
Point::new(start_row, 0).to_offset(&snapshot)..Point::new(end_row, 0).to_offset(&snapshot);
|
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()) {
|
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();
|
let mut buffer_text = String::new();
|
||||||
for chunk in snapshot.text_for_range(excerpt_range) {
|
for chunk in snapshot.text_for_range(excerpt_range) {
|
||||||
|
@ -461,46 +399,26 @@ fn collect_diagnostic(
|
||||||
|
|
||||||
for (i, line) in buffer_text.lines().enumerate() {
|
for (i, line) in buffer_text.lines().enumerate() {
|
||||||
let line_number = start_row + i as u32 + 1;
|
let line_number = start_row + i as u32 + 1;
|
||||||
writeln!(text, "{}", line).unwrap();
|
writeln!(output.text, "{}", line).unwrap();
|
||||||
|
|
||||||
if line_number == diagnostic_row_number {
|
if line_number == diagnostic_row_number {
|
||||||
text.push_str("//");
|
output.text.push_str("//");
|
||||||
let prev_len = text.len();
|
let prev_len = output.text.len();
|
||||||
write!(text, " {}: ", ty.as_str()).unwrap();
|
write!(output.text, " {}: ", ty).unwrap();
|
||||||
let padding = text.len() - prev_len;
|
let padding = output.text.len() - prev_len;
|
||||||
|
|
||||||
let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH)
|
let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH)
|
||||||
.replace('\n', format!("\n//{:padding$}", "").as_str());
|
.replace('\n', format!("\n//{:padding$}", "").as_str());
|
||||||
|
|
||||||
writeln!(text, "{message}").unwrap();
|
writeln!(output.text, "{message}").unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeln!(text, "```").unwrap();
|
writeln!(output.text, "```").unwrap();
|
||||||
sections.push((
|
output.sections.push(SlashCommandOutputSection {
|
||||||
prev_len..text.len().saturating_sub(1),
|
range: prev_len..output.text.len().saturating_sub(1),
|
||||||
PlaceholderType::Diagnostic(ty, entry.diagnostic.message.clone()),
|
icon,
|
||||||
))
|
label: entry.diagnostic.message.clone().into(),
|
||||||
}
|
metadata: None,
|
||||||
|
});
|
||||||
#[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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use indexed_docs::{
|
||||||
DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
|
DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
|
||||||
ProviderId,
|
ProviderId,
|
||||||
};
|
};
|
||||||
use language::LspAdapterDelegate;
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
use project::{Project, ProjectPath};
|
use project::{Project, ProjectPath};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use util::{maybe, ResultExt};
|
use util::{maybe, ResultExt};
|
||||||
|
@ -269,6 +269,8 @@ impl SlashCommand for DocsSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -349,6 +351,7 @@ impl SlashCommand for DocsSlashCommand {
|
||||||
range,
|
range,
|
||||||
icon: IconName::FileDoc,
|
icon: IconName::FileDoc,
|
||||||
label: format!("docs ({provider}): {key}",).into(),
|
label: format!("docs ({provider}): {key}",).into(),
|
||||||
|
metadata: None,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
|
|
|
@ -11,7 +11,7 @@ use futures::AsyncReadExt;
|
||||||
use gpui::{Task, WeakView};
|
use gpui::{Task, WeakView};
|
||||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||||
use language::LspAdapterDelegate;
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
@ -128,6 +128,8 @@ impl SlashCommand for FetchSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -161,6 +163,7 @@ impl SlashCommand for FetchSlashCommand {
|
||||||
range,
|
range,
|
||||||
icon: IconName::AtSign,
|
icon: IconName::AtSign,
|
||||||
label: format!("fetch {}", url).into(),
|
label: format!("fetch {}", url).into(),
|
||||||
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 anyhow::{anyhow, Context as _, Result};
|
||||||
use assistant_slash_command::{AfterCompletion, ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{AfterCompletion, ArgumentCompletion, SlashCommandOutputSection};
|
||||||
use fuzzy::PathMatch;
|
use fuzzy::PathMatch;
|
||||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||||
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
|
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
|
||||||
use project::{PathMatchCandidateSet, Project};
|
use project::{PathMatchCandidateSet, Project};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
|
@ -175,6 +176,8 @@ impl SlashCommand for FileSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -187,54 +190,15 @@ impl SlashCommand for FileSlashCommand {
|
||||||
return Task::ready(Err(anyhow!("missing path")));
|
return Task::ready(Err(anyhow!("missing path")));
|
||||||
};
|
};
|
||||||
|
|
||||||
let task = collect_files(workspace.read(cx).project().clone(), arguments, cx);
|
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,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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(
|
fn collect_files(
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
glob_inputs: &[String],
|
glob_inputs: &[String],
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Task<Result<FileCommandOutput>> {
|
) -> Task<Result<SlashCommandOutput>> {
|
||||||
let Ok(matchers) = glob_inputs
|
let Ok(matchers) = glob_inputs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|glob_input| {
|
.map(|glob_input| {
|
||||||
|
@ -254,8 +218,7 @@ fn collect_files(
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let mut text = String::new();
|
let mut output = SlashCommandOutput::default();
|
||||||
let mut ranges = Vec::new();
|
|
||||||
for snapshot in snapshots {
|
for snapshot in snapshots {
|
||||||
let worktree_id = snapshot.id();
|
let worktree_id = snapshot.id();
|
||||||
let mut directory_stack: Vec<(Arc<Path>, String, usize)> = Vec::new();
|
let mut directory_stack: Vec<(Arc<Path>, String, usize)> = Vec::new();
|
||||||
|
@ -279,11 +242,12 @@ fn collect_files(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let (_, entry_name, start) = directory_stack.pop().unwrap();
|
let (_, entry_name, start) = directory_stack.pop().unwrap();
|
||||||
ranges.push(OutputFile {
|
output.sections.push(build_entry_output_section(
|
||||||
range_in_text: start..text.len().saturating_sub(1),
|
start..output.text.len().saturating_sub(1),
|
||||||
path: PathBuf::from(entry_name),
|
Some(&PathBuf::from(entry_name)),
|
||||||
entry_type: EntryType::Directory,
|
true,
|
||||||
});
|
None,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let filename = entry
|
let filename = entry
|
||||||
|
@ -315,21 +279,23 @@ fn collect_files(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/");
|
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 prefix_paths.is_empty() {
|
||||||
if is_top_level_directory {
|
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;
|
is_top_level_directory = false;
|
||||||
} else {
|
} else {
|
||||||
text.push_str(&filename);
|
output.text.push_str(&filename);
|
||||||
}
|
}
|
||||||
directory_stack.push((entry.path.clone(), filename, entry_start));
|
directory_stack.push((entry.path.clone(), filename, entry_start));
|
||||||
} else {
|
} else {
|
||||||
let entry_name = format!("{}/{}", prefix_paths, &filename);
|
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));
|
directory_stack.push((entry.path.clone(), entry_name, entry_start));
|
||||||
}
|
}
|
||||||
text.push('\n');
|
output.text.push('\n');
|
||||||
} else if entry.is_file() {
|
} else if entry.is_file() {
|
||||||
let Some(open_buffer_task) = project_handle
|
let Some(open_buffer_task) = project_handle
|
||||||
.update(&mut cx, |project, cx| {
|
.update(&mut cx, |project, cx| {
|
||||||
|
@ -340,28 +306,13 @@ fn collect_files(
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if let Some(buffer) = open_buffer_task.await.log_err() {
|
if let Some(buffer) = open_buffer_task.await.log_err() {
|
||||||
let buffer_snapshot =
|
let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?;
|
||||||
cx.read_model(&buffer, |buffer, _| buffer.snapshot())?;
|
append_buffer_to_output(
|
||||||
let prev_len = text.len();
|
&snapshot,
|
||||||
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,
|
|
||||||
Some(&path_including_worktree_name),
|
Some(&path_including_worktree_name),
|
||||||
&buffer_snapshot,
|
&mut output,
|
||||||
) {
|
)
|
||||||
text.pop();
|
.log_err();
|
||||||
}
|
|
||||||
ranges.push(OutputFile {
|
|
||||||
range_in_text: prev_len..text.len(),
|
|
||||||
path: path_including_worktree_name,
|
|
||||||
entry_type: EntryType::File,
|
|
||||||
});
|
|
||||||
text.push('\n');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -371,42 +322,26 @@ fn collect_files(
|
||||||
let mut root_path = PathBuf::new();
|
let mut root_path = PathBuf::new();
|
||||||
root_path.push(snapshot.root_name());
|
root_path.push(snapshot.root_name());
|
||||||
root_path.push(&dir);
|
root_path.push(&dir);
|
||||||
ranges.push(OutputFile {
|
output.sections.push(build_entry_output_section(
|
||||||
range_in_text: start..text.len(),
|
start..output.text.len(),
|
||||||
path: root_path,
|
Some(&root_path),
|
||||||
entry_type: EntryType::Directory,
|
true,
|
||||||
});
|
None,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
ranges.push(OutputFile {
|
output.sections.push(build_entry_output_section(
|
||||||
range_in_text: start..text.len(),
|
start..output.text.len(),
|
||||||
path: PathBuf::from(entry.as_str()),
|
Some(&PathBuf::from(entry.as_str())),
|
||||||
entry_type: EntryType::Directory,
|
true,
|
||||||
});
|
None,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(FileCommandOutput {
|
Ok(output)
|
||||||
completion_text: text,
|
|
||||||
files: ranges,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
pub fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<Range<u32>>) -> String {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
write!(text, "```").unwrap();
|
write!(text, "```").unwrap();
|
||||||
|
@ -429,6 +364,11 @@ pub fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<Range<u32
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct FileCommandMetadata {
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_entry_output_section(
|
pub fn build_entry_output_section(
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
path: Option<&Path>,
|
path: Option<&Path>,
|
||||||
|
@ -454,6 +394,16 @@ pub fn build_entry_output_section(
|
||||||
range,
|
range,
|
||||||
icon,
|
icon,
|
||||||
label: label.into(),
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
|
@ -591,9 +571,9 @@ mod test {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(result_1.completion_text.starts_with("root/dir"));
|
assert!(result_1.text.starts_with("root/dir"));
|
||||||
// 4 files + 2 directories
|
// 4 files + 2 directories
|
||||||
assert_eq!(6, result_1.files.len());
|
assert_eq!(result_1.sections.len(), 6);
|
||||||
|
|
||||||
let result_2 = cx
|
let result_2 = cx
|
||||||
.update(|cx| collect_files(project.clone(), &["root/dir/".to_string()], cx))
|
.update(|cx| collect_files(project.clone(), &["root/dir/".to_string()], cx))
|
||||||
|
@ -607,9 +587,9 @@ mod test {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(result.completion_text.starts_with("root/dir"));
|
assert!(result.text.starts_with("root/dir"));
|
||||||
// 5 files + 2 directories
|
// 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
|
// Ensure that the project lasts until after the last await
|
||||||
drop(project);
|
drop(project);
|
||||||
|
@ -654,36 +634,27 @@ mod test {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
assert!(result.completion_text.starts_with("zed/assets/themes\n"));
|
assert!(result.text.starts_with("zed/assets/themes\n"));
|
||||||
assert_eq!(7, result.files.len());
|
assert_eq!(result.sections.len(), 7);
|
||||||
|
|
||||||
// Ensure that full file paths are included in the real output
|
// Ensure that full file paths are included in the real output
|
||||||
assert!(result
|
assert!(result.text.contains("zed/assets/themes/andromeda/LICENSE"));
|
||||||
.completion_text
|
assert!(result.text.contains("zed/assets/themes/ayu/LICENSE"));
|
||||||
.contains("zed/assets/themes/andromeda/LICENSE"));
|
assert!(result.text.contains("zed/assets/themes/summercamp/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_eq!(result.sections[5].label, "summercamp");
|
||||||
|
|
||||||
// Ensure that things are in descending order, with properly relativized paths
|
// Ensure that things are in descending order, with properly relativized paths
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"zed/assets/themes/andromeda/LICENSE",
|
result.sections[0].label,
|
||||||
result.files[0].path.to_string_lossy()
|
"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!(
|
assert_eq!(
|
||||||
"zed/assets/themes/ayu/LICENSE",
|
result.sections[4].label,
|
||||||
result.files[2].path.to_string_lossy()
|
"zed/assets/themes/summercamp/LICENSE"
|
||||||
);
|
|
||||||
assert_eq!("ayu", result.files[3].path.to_string_lossy());
|
|
||||||
assert_eq!(
|
|
||||||
"zed/assets/themes/summercamp/LICENSE",
|
|
||||||
result.files[4].path.to_string_lossy()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ensure that the project lasts until after the last await
|
// Ensure that the project lasts until after the last await
|
||||||
|
@ -723,27 +694,24 @@ mod test {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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!(
|
assert_eq!(
|
||||||
"zed/assets/themes/LICENSE",
|
result.sections[1].label,
|
||||||
result.files[0].path.to_string_lossy()
|
"zed/assets/themes/summercamp/LICENSE"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"zed/assets/themes/summercamp/LICENSE",
|
result.sections[2].label,
|
||||||
result.files[1].path.to_string_lossy()
|
"zed/assets/themes/summercamp/subdir/LICENSE"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"zed/assets/themes/summercamp/subdir/LICENSE",
|
result.sections[3].label,
|
||||||
result.files[2].path.to_string_lossy()
|
"zed/assets/themes/summercamp/subdir/subsubdir/LICENSE"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(result.sections[4].label, "subsubdir");
|
||||||
"zed/assets/themes/summercamp/subdir/subsubdir/LICENSE",
|
assert_eq!(result.sections[5].label, "subdir");
|
||||||
result.files[3].path.to_string_lossy()
|
assert_eq!(result.sections[6].label, "summercamp");
|
||||||
);
|
assert_eq!(result.sections[7].label, "zed/assets/themes");
|
||||||
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());
|
|
||||||
|
|
||||||
// Ensure that the project lasts until after the last await
|
// Ensure that the project lasts until after the last await
|
||||||
drop(project);
|
drop(project);
|
||||||
|
|
|
@ -7,7 +7,7 @@ use assistant_slash_command::{
|
||||||
};
|
};
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use gpui::{Task, WeakView};
|
use gpui::{Task, WeakView};
|
||||||
use language::LspAdapterDelegate;
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
@ -43,6 +43,8 @@ impl SlashCommand for NowSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
_arguments: &[String],
|
_arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
_cx: &mut WindowContext,
|
_cx: &mut WindowContext,
|
||||||
|
@ -57,6 +59,7 @@ impl SlashCommand for NowSlashCommand {
|
||||||
range,
|
range,
|
||||||
icon: IconName::CountdownTimer,
|
icon: IconName::CountdownTimer,
|
||||||
label: now.to_rfc2822().into(),
|
label: now.to_rfc2822().into(),
|
||||||
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -3,7 +3,7 @@ use anyhow::{anyhow, Context, Result};
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{AppContext, Model, Task, WeakView};
|
use gpui::{AppContext, Model, Task, WeakView};
|
||||||
use language::LspAdapterDelegate;
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
use project::{Project, ProjectPath};
|
use project::{Project, ProjectPath};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
|
@ -118,6 +118,8 @@ impl SlashCommand for ProjectSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
_arguments: &[String],
|
_arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -140,6 +142,7 @@ impl SlashCommand for ProjectSlashCommand {
|
||||||
range,
|
range,
|
||||||
icon: IconName::FileTree,
|
icon: IconName::FileTree,
|
||||||
label: "Project".into(),
|
label: "Project".into(),
|
||||||
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::prompt_library::PromptStore;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||||
use gpui::{Task, WeakView};
|
use gpui::{Task, WeakView};
|
||||||
use language::LspAdapterDelegate;
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
use std::sync::{atomic::AtomicBool, Arc};
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
@ -56,6 +56,8 @@ impl SlashCommand for PromptSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -95,6 +97,7 @@ impl SlashCommand for PromptSlashCommand {
|
||||||
range,
|
range,
|
||||||
icon: IconName::Library,
|
icon: IconName::Library,
|
||||||
label: title,
|
label: title,
|
||||||
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
run_commands_in_text: true,
|
run_commands_in_text: true,
|
||||||
})
|
})
|
||||||
|
|
|
@ -60,6 +60,8 @@ impl SlashCommand for SearchSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: language::BufferSnapshot,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -168,6 +170,7 @@ impl SlashCommand for SearchSlashCommand {
|
||||||
range: 0..text.len(),
|
range: 0..text.len(),
|
||||||
icon: IconName::MagnifyingGlass,
|
icon: IconName::MagnifyingGlass,
|
||||||
label: query,
|
label: query,
|
||||||
|
metadata: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
SlashCommandOutput {
|
SlashCommandOutput {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use anyhow::{anyhow, Context as _, Result};
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{Task, WeakView};
|
use gpui::{Task, WeakView};
|
||||||
use language::LspAdapterDelegate;
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{path::Path, sync::atomic::AtomicBool};
|
use std::{path::Path, sync::atomic::AtomicBool};
|
||||||
use ui::{IconName, WindowContext};
|
use ui::{IconName, WindowContext};
|
||||||
|
@ -41,6 +41,8 @@ impl SlashCommand for OutlineSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
_arguments: &[String],
|
_arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -77,6 +79,7 @@ impl SlashCommand for OutlineSlashCommand {
|
||||||
range: 0..outline_text.len(),
|
range: 0..outline_text.len(),
|
||||||
icon: IconName::ListTree,
|
icon: IconName::ListTree,
|
||||||
label: path.to_string_lossy().to_string().into(),
|
label: path.to_string_lossy().to_string().into(),
|
||||||
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
text: outline_text,
|
text: outline_text,
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
|
|
|
@ -1,21 +1,17 @@
|
||||||
use super::{
|
use super::{file_command::append_buffer_to_output, SlashCommand, SlashCommandOutput};
|
||||||
diagnostics_command::write_single_file_diagnostics,
|
|
||||||
file_command::{build_entry_output_section, codeblock_fence_for_path},
|
|
||||||
SlashCommand, SlashCommandOutput,
|
|
||||||
};
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use assistant_slash_command::ArgumentCompletion;
|
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use gpui::{Entity, Task, WeakView};
|
use gpui::{Entity, Task, WeakView};
|
||||||
use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
|
use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Write,
|
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
};
|
};
|
||||||
use ui::{ActiveTheme, WindowContext};
|
use ui::{ActiveTheme, WindowContext};
|
||||||
|
use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub(crate) struct TabSlashCommand;
|
pub(crate) struct TabSlashCommand;
|
||||||
|
@ -131,6 +127,8 @@ impl SlashCommand for TabSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -144,40 +142,11 @@ impl SlashCommand for TabSlashCommand {
|
||||||
);
|
);
|
||||||
|
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
let mut sections = Vec::new();
|
let mut output = SlashCommandOutput::default();
|
||||||
let mut text = String::new();
|
|
||||||
let mut has_diagnostics = false;
|
|
||||||
for (full_path, buffer, _) in tab_items_search.await? {
|
for (full_path, buffer, _) in tab_items_search.await? {
|
||||||
let section_start_ix = text.len();
|
append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err();
|
||||||
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,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
Ok(output)
|
||||||
Ok(SlashCommandOutput {
|
|
||||||
text,
|
|
||||||
sections,
|
|
||||||
run_commands_in_text: has_diagnostics,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use assistant_slash_command::{
|
||||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
};
|
};
|
||||||
use gpui::{AppContext, Task, View, WeakView};
|
use gpui::{AppContext, Task, View, WeakView};
|
||||||
use language::{CodeLabel, LspAdapterDelegate};
|
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use workspace::{dock::Panel, Workspace};
|
use workspace::{dock::Panel, Workspace};
|
||||||
|
@ -57,6 +57,8 @@ impl SlashCommand for TerminalSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -91,6 +93,7 @@ impl SlashCommand for TerminalSlashCommand {
|
||||||
range,
|
range,
|
||||||
icon: IconName::Terminal,
|
icon: IconName::Terminal,
|
||||||
label: "Terminal".into(),
|
label: "Terminal".into(),
|
||||||
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -8,7 +8,7 @@ use assistant_slash_command::{
|
||||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
};
|
};
|
||||||
use gpui::{Task, WeakView};
|
use gpui::{Task, WeakView};
|
||||||
use language::LspAdapterDelegate;
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
@ -53,6 +53,8 @@ impl SlashCommand for WorkflowSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
_arguments: &[String],
|
_arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -68,6 +70,7 @@ impl SlashCommand for WorkflowSlashCommand {
|
||||||
range,
|
range,
|
||||||
icon: IconName::Route,
|
icon: IconName::Route,
|
||||||
label: "Workflow".into(),
|
label: "Workflow".into(),
|
||||||
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,4 +19,5 @@ gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
|
@ -2,7 +2,7 @@ mod slash_command_registry;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
|
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
|
||||||
use language::{CodeLabel, LspAdapterDelegate};
|
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
pub use slash_command_registry::*;
|
pub use slash_command_registry::*;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -77,6 +77,8 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
context_buffer: BufferSnapshot,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
// TODO: We're just using the `LspAdapterDelegate` here because that is
|
// TODO: We're just using the `LspAdapterDelegate` here because that is
|
||||||
// what the extension API is already expecting.
|
// 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,
|
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default, PartialEq)]
|
||||||
pub struct SlashCommandOutput {
|
pub struct SlashCommandOutput {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
||||||
|
@ -106,4 +108,11 @@ pub struct SlashCommandOutputSection<T> {
|
||||||
pub range: Range<T>,
|
pub range: Range<T>,
|
||||||
pub icon: IconName,
|
pub icon: IconName,
|
||||||
pub label: SharedString,
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use assistant_slash_command::{
|
||||||
};
|
};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use gpui::{Task, WeakView, WindowContext};
|
use gpui::{Task, WeakView, WindowContext};
|
||||||
use language::LspAdapterDelegate;
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use wasmtime_wasi::WasiView;
|
use wasmtime_wasi::WasiView;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
@ -82,6 +82,8 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -121,6 +123,7 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||||
range: section.range.into(),
|
range: section.range.into(),
|
||||||
icon: IconName::Code,
|
icon: IconName::Code,
|
||||||
label: section.label.into(),
|
label: section.label.into(),
|
||||||
|
metadata: None,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
|
|
|
@ -2390,6 +2390,7 @@ message SlashCommandOutputSection {
|
||||||
AnchorRange range = 1;
|
AnchorRange range = 1;
|
||||||
string icon_name = 2;
|
string icon_name = 2;
|
||||||
string label = 3;
|
string label = 3;
|
||||||
|
optional string metadata = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ContextOperation {
|
message ContextOperation {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue