Add slash commands for adding context into the assistant (#12102)

Tasks

* [x] remove old flaps and output when editing a slash command
* [x] the completing a command name that takes args, insert a space to
prepare for typing an arg
* [x] always trigger completions when  typing in a slash command
* [x] don't show line numbers
* [x] implement `prompt` command
* [x] `current-file` command
* [x] state gets corrupted on `duplicate line up` on a slash command
* [x] exclude slash command source from completion request

Next steps:
* show output token count in flap trailer
* add `/project` command that matches project ambient context
* delete ambient context

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
Max Brunsfeld 2024-05-22 14:06:28 -07:00 committed by GitHub
parent d6e59bfae1
commit a73a3ef243
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1649 additions and 147 deletions

View file

@ -0,0 +1,145 @@
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
use anyhow::Result;
use futures::channel::oneshot;
use fuzzy::PathMatch;
use gpui::{AppContext, Model, Task};
use project::{PathMatchCandidateSet, Project};
use std::{
path::Path,
sync::{atomic::AtomicBool, Arc},
};
pub(crate) struct FileSlashCommand {
project: Model<Project>,
}
impl FileSlashCommand {
pub fn new(project: Model<Project>) -> Self {
Self { project }
}
fn search_paths(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
cx: &mut AppContext,
) -> Task<Vec<PathMatch>> {
let worktrees = self
.project
.read(cx)
.visible_worktrees(cx)
.collect::<Vec<_>>();
let include_root_name = worktrees.len() > 1;
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name,
directories_only: false,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
impl SlashCommand for FileSlashCommand {
fn name(&self) -> String {
"file".into()
}
fn description(&self) -> String {
"insert an entire file".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
cx: &mut AppContext,
) -> gpui::Task<Result<Vec<String>>> {
let paths = self.search_paths(query, cancellation_flag, cx);
cx.background_executor().spawn(async move {
Ok(paths
.await
.into_iter()
.map(|path_match| {
format!(
"{}{}",
path_match.path_prefix,
path_match.path.to_string_lossy()
)
})
.collect())
})
}
fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
let project = self.project.read(cx);
let Some(argument) = argument else {
return SlashCommandInvocation {
output: Task::ready(Err(anyhow::anyhow!("missing path"))),
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
};
};
let path = Path::new(argument);
let abs_path = project.worktrees().find_map(|worktree| {
let worktree = worktree.read(cx);
worktree.entry_for_path(path)?;
worktree.absolutize(path).ok()
});
let Some(abs_path) = abs_path else {
return SlashCommandInvocation {
output: Task::ready(Err(anyhow::anyhow!("missing path"))),
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
};
};
let fs = project.fs().clone();
let argument = argument.to_string();
let output = cx.background_executor().spawn(async move {
let content = fs.load(&abs_path).await?;
let mut output = String::with_capacity(argument.len() + content.len() + 9);
output.push_str("```");
output.push_str(&argument);
output.push('\n');
output.push_str(&content);
if !output.ends_with('\n') {
output.push('\n');
}
output.push_str("```");
Ok(output)
});
SlashCommandInvocation {
output,
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
}
}
}