Skip to content

Commit e309fbd

Browse files
maxbrunsfeldjvmncsrtfeldman
authored
Add a slash command for automatically retrieving relevant context (#17972)
* [x] put this slash command behind a feature flag until we release embedding access to the general population * [x] choose a name for this slash command and name the rust module to match Release Notes: - N/A --------- Co-authored-by: Jason <jason@zed.dev> Co-authored-by: Richard <richard@zed.dev> Co-authored-by: Jason Mancuso <7891333+jvmncs@users.noreply.github.com> Co-authored-by: Richard Feldman <oss@rtfeldman.com>
1 parent 5905fbb commit e309fbd

File tree

14 files changed

+680
-220
lines changed

14 files changed

+680
-220
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
A software developer is asking a question about their project. The source files in their project have been indexed into a database of semantic text embeddings.
2+
Your task is to generate a list of 4 diverse search queries that can be run on this embedding database, in order to retrieve a list of code snippets
3+
that are relevant to the developer's question. Redundant search queries will be heavily penalized, so only include another query if it's sufficiently
4+
distinct from previous ones.
5+
6+
Here is the question that's been asked, together with context that the developer has added manually:
7+
8+
{{{context_buffer}}}

crates/assistant/src/assistant.rs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ use semantic_index::{CloudEmbeddingProvider, SemanticDb};
4141
use serde::{Deserialize, Serialize};
4242
use settings::{update_settings_file, Settings, SettingsStore};
4343
use slash_command::{
44-
auto_command, context_server_command, default_command, delta_command, diagnostics_command,
45-
docs_command, fetch_command, file_command, now_command, project_command, prompt_command,
46-
search_command, symbols_command, tab_command, terminal_command, workflow_command,
44+
auto_command, cargo_workspace_command, context_server_command, default_command, delta_command,
45+
diagnostics_command, docs_command, fetch_command, file_command, now_command, project_command,
46+
prompt_command, search_command, symbols_command, tab_command, terminal_command,
47+
workflow_command,
4748
};
4849
use std::path::PathBuf;
4950
use std::sync::Arc;
@@ -384,20 +385,33 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
384385
slash_command_registry.register_command(delta_command::DeltaSlashCommand, true);
385386
slash_command_registry.register_command(symbols_command::OutlineSlashCommand, true);
386387
slash_command_registry.register_command(tab_command::TabSlashCommand, true);
387-
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
388+
slash_command_registry
389+
.register_command(cargo_workspace_command::CargoWorkspaceSlashCommand, true);
388390
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
389391
slash_command_registry.register_command(default_command::DefaultSlashCommand, false);
390392
slash_command_registry.register_command(terminal_command::TerminalSlashCommand, true);
391393
slash_command_registry.register_command(now_command::NowSlashCommand, false);
392394
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
395+
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
393396

394397
if let Some(prompt_builder) = prompt_builder {
395398
slash_command_registry.register_command(
396399
workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
397400
true,
398401
);
402+
cx.observe_flag::<project_command::ProjectSlashCommandFeatureFlag, _>({
403+
let slash_command_registry = slash_command_registry.clone();
404+
move |is_enabled, _cx| {
405+
if is_enabled {
406+
slash_command_registry.register_command(
407+
project_command::ProjectSlashCommand::new(prompt_builder.clone()),
408+
true,
409+
);
410+
}
411+
}
412+
})
413+
.detach();
399414
}
400-
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
401415

402416
cx.observe_flag::<auto_command::AutoSlashCommandFeatureFlag, _>({
403417
let slash_command_registry = slash_command_registry.clone();
@@ -435,10 +449,12 @@ fn update_slash_commands_from_settings(cx: &mut AppContext) {
435449
slash_command_registry.unregister_command(docs_command::DocsSlashCommand);
436450
}
437451

438-
if settings.project.enabled {
439-
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
452+
if settings.cargo_workspace.enabled {
453+
slash_command_registry
454+
.register_command(cargo_workspace_command::CargoWorkspaceSlashCommand, true);
440455
} else {
441-
slash_command_registry.unregister_command(project_command::ProjectSlashCommand);
456+
slash_command_registry
457+
.unregister_command(cargo_workspace_command::CargoWorkspaceSlashCommand);
442458
}
443459
}
444460

crates/assistant/src/context.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1967,8 +1967,9 @@ impl Context {
19671967
}
19681968

19691969
pub fn assist(&mut self, cx: &mut ModelContext<Self>) -> Option<MessageAnchor> {
1970-
let provider = LanguageModelRegistry::read_global(cx).active_provider()?;
1971-
let model = LanguageModelRegistry::read_global(cx).active_model()?;
1970+
let model_registry = LanguageModelRegistry::read_global(cx);
1971+
let provider = model_registry.active_provider()?;
1972+
let model = model_registry.active_model()?;
19721973
let last_message_id = self.get_last_valid_message_id(cx)?;
19731974

19741975
if !provider.is_authenticated(cx) {

crates/assistant/src/prompts.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ pub struct TerminalAssistantPromptContext {
4040
pub user_prompt: String,
4141
}
4242

43+
#[derive(Serialize)]
44+
pub struct ProjectSlashCommandPromptContext {
45+
pub context_buffer: String,
46+
}
47+
4348
/// Context required to generate a workflow step resolution prompt.
4449
#[derive(Debug, Serialize)]
4550
pub struct StepResolutionContext {
@@ -317,4 +322,14 @@ impl PromptBuilder {
317322
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
318323
self.handlebars.lock().render("edit_workflow", &())
319324
}
325+
326+
pub fn generate_project_slash_command_prompt(
327+
&self,
328+
context_buffer: String,
329+
) -> Result<String, RenderError> {
330+
self.handlebars.lock().render(
331+
"project_slash_command",
332+
&ProjectSlashCommandPromptContext { context_buffer },
333+
)
334+
}
320335
}

crates/assistant/src/slash_command.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ use std::{
1818
};
1919
use ui::ActiveTheme;
2020
use workspace::Workspace;
21-
2221
pub mod auto_command;
22+
pub mod cargo_workspace_command;
2323
pub mod context_server_command;
2424
pub mod default_command;
2525
pub mod delta_command;
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
use super::{SlashCommand, SlashCommandOutput};
2+
use anyhow::{anyhow, Context, Result};
3+
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
4+
use fs::Fs;
5+
use gpui::{AppContext, Model, Task, WeakView};
6+
use language::{BufferSnapshot, LspAdapterDelegate};
7+
use project::{Project, ProjectPath};
8+
use std::{
9+
fmt::Write,
10+
path::Path,
11+
sync::{atomic::AtomicBool, Arc},
12+
};
13+
use ui::prelude::*;
14+
use workspace::Workspace;
15+
16+
pub(crate) struct CargoWorkspaceSlashCommand;
17+
18+
impl CargoWorkspaceSlashCommand {
19+
async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
20+
let buffer = fs.load(path_to_cargo_toml).await?;
21+
let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
22+
23+
let mut message = String::new();
24+
writeln!(message, "You are in a Rust project.")?;
25+
26+
if let Some(workspace) = cargo_toml.workspace {
27+
writeln!(
28+
message,
29+
"The project is a Cargo workspace with the following members:"
30+
)?;
31+
for member in workspace.members {
32+
writeln!(message, "- {member}")?;
33+
}
34+
35+
if !workspace.default_members.is_empty() {
36+
writeln!(message, "The default members are:")?;
37+
for member in workspace.default_members {
38+
writeln!(message, "- {member}")?;
39+
}
40+
}
41+
42+
if !workspace.dependencies.is_empty() {
43+
writeln!(
44+
message,
45+
"The following workspace dependencies are installed:"
46+
)?;
47+
for dependency in workspace.dependencies.keys() {
48+
writeln!(message, "- {dependency}")?;
49+
}
50+
}
51+
} else if let Some(package) = cargo_toml.package {
52+
writeln!(
53+
message,
54+
"The project name is \"{name}\".",
55+
name = package.name
56+
)?;
57+
58+
let description = package
59+
.description
60+
.as_ref()
61+
.and_then(|description| description.get().ok().cloned());
62+
if let Some(description) = description.as_ref() {
63+
writeln!(message, "It describes itself as \"{description}\".")?;
64+
}
65+
66+
if !cargo_toml.dependencies.is_empty() {
67+
writeln!(message, "The following dependencies are installed:")?;
68+
for dependency in cargo_toml.dependencies.keys() {
69+
writeln!(message, "- {dependency}")?;
70+
}
71+
}
72+
}
73+
74+
Ok(message)
75+
}
76+
77+
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
78+
let worktree = project.read(cx).worktrees(cx).next()?;
79+
let worktree = worktree.read(cx);
80+
let entry = worktree.entry_for_path("Cargo.toml")?;
81+
let path = ProjectPath {
82+
worktree_id: worktree.id(),
83+
path: entry.path.clone(),
84+
};
85+
Some(Arc::from(
86+
project.read(cx).absolute_path(&path, cx)?.as_path(),
87+
))
88+
}
89+
}
90+
91+
impl SlashCommand for CargoWorkspaceSlashCommand {
92+
fn name(&self) -> String {
93+
"cargo-workspace".into()
94+
}
95+
96+
fn description(&self) -> String {
97+
"insert project workspace metadata".into()
98+
}
99+
100+
fn menu_text(&self) -> String {
101+
"Insert Project Workspace Metadata".into()
102+
}
103+
104+
fn complete_argument(
105+
self: Arc<Self>,
106+
_arguments: &[String],
107+
_cancel: Arc<AtomicBool>,
108+
_workspace: Option<WeakView<Workspace>>,
109+
_cx: &mut WindowContext,
110+
) -> Task<Result<Vec<ArgumentCompletion>>> {
111+
Task::ready(Err(anyhow!("this command does not require argument")))
112+
}
113+
114+
fn requires_argument(&self) -> bool {
115+
false
116+
}
117+
118+
fn run(
119+
self: Arc<Self>,
120+
_arguments: &[String],
121+
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
122+
_context_buffer: BufferSnapshot,
123+
workspace: WeakView<Workspace>,
124+
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
125+
cx: &mut WindowContext,
126+
) -> Task<Result<SlashCommandOutput>> {
127+
let output = workspace.update(cx, |workspace, cx| {
128+
let project = workspace.project().clone();
129+
let fs = workspace.project().read(cx).fs().clone();
130+
let path = Self::path_to_cargo_toml(project, cx);
131+
let output = cx.background_executor().spawn(async move {
132+
let path = path.with_context(|| "Cargo.toml not found")?;
133+
Self::build_message(fs, &path).await
134+
});
135+
136+
cx.foreground_executor().spawn(async move {
137+
let text = output.await?;
138+
let range = 0..text.len();
139+
Ok(SlashCommandOutput {
140+
text,
141+
sections: vec![SlashCommandOutputSection {
142+
range,
143+
icon: IconName::FileTree,
144+
label: "Project".into(),
145+
metadata: None,
146+
}],
147+
run_commands_in_text: false,
148+
})
149+
})
150+
});
151+
output.unwrap_or_else(|error| Task::ready(Err(error)))
152+
}
153+
}

0 commit comments

Comments
 (0)