mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-16 00:47:39 +03:00
Introduce /search
command to assistant (#12372)
This pull request introduces semantic search to the assistant using a slash command: https://github.com/zed-industries/zed/assets/482957/62f39eae-d7d5-46bf-a356-dd081ff88312 Moreover, this also adds a status to pending slash commands, so that we can show when a query is running or whether it failed: <img width="1588" alt="image" src="https://github.com/zed-industries/zed/assets/482957/e8d85960-6275-4552-a068-85efb74cfde1"> I think this could be better design-wise, but seems like a pretty good start. Release Notes: - N/A
This commit is contained in:
parent
016a1444a7
commit
59662fbeb6
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -368,6 +368,7 @@ dependencies = [
|
||||
"rope",
|
||||
"schemars",
|
||||
"search",
|
||||
"semantic_index",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
|
@ -41,6 +41,7 @@ regex.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
|
@ -16,12 +16,14 @@ use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
pub(crate) use saved_conversation::*;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::paths::EMBEDDINGS_DIR;
|
||||
|
||||
actions!(
|
||||
assistant,
|
||||
@ -232,6 +234,21 @@ impl Assistant {
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
cx.set_global(Assistant::default());
|
||||
AssistantSettings::register(cx);
|
||||
|
||||
cx.spawn(|mut cx| {
|
||||
let client = client.clone();
|
||||
async move {
|
||||
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
|
||||
let semantic_index = SemanticIndex::new(
|
||||
EMBEDDINGS_DIR.join("semantic-index-db.0.mdb"),
|
||||
Arc::new(embedding_provider),
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
cx.update(|cx| cx.set_global(semantic_index))
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
completion_provider::init(client, cx);
|
||||
assistant_slash_command::init(cx);
|
||||
assistant_panel::init(cx);
|
||||
|
@ -1,4 +1,5 @@
|
||||
use crate::prompts::{generate_content_prompt, PromptLibrary, PromptManager};
|
||||
use crate::slash_command::search_command;
|
||||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel},
|
||||
codegen::{self, Codegen, CodegenKind},
|
||||
@ -13,9 +14,10 @@ use crate::{
|
||||
SavedMessage, Split, ToggleFocus, ToggleHistory, ToggleIncludeConversation,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{RenderFoldPlaceholder, SlashCommandOutput};
|
||||
use assistant_slash_command::{SlashCommandOutput, SlashCommandOutputSection};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{hash_map, HashMap, HashSet, VecDeque};
|
||||
use collections::{hash_map, BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use editor::actions::UnfoldAt;
|
||||
use editor::{
|
||||
actions::{FoldAt, MoveDown, MoveUp},
|
||||
display_map::{
|
||||
@ -28,7 +30,8 @@ use editor::{
|
||||
use editor::{display_map::FlapId, FoldPlaceholder};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AnyView, AppContext,
|
||||
AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, Empty,
|
||||
@ -38,11 +41,11 @@ use gpui::{
|
||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace,
|
||||
WindowContext,
|
||||
};
|
||||
use language::LspAdapterDelegate;
|
||||
use language::{
|
||||
language_settings::SoftWrap, AutoindentMode, Buffer, LanguageRegistry, OffsetRangeExt as _,
|
||||
Point, ToOffset as _,
|
||||
language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry,
|
||||
OffsetRangeExt as _, Point, ToOffset as _,
|
||||
};
|
||||
use language::{LineEnding, LspAdapterDelegate};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction};
|
||||
@ -208,6 +211,7 @@ impl AssistantPanel {
|
||||
);
|
||||
slash_command_registry.register_command(active_command::ActiveSlashCommand);
|
||||
slash_command_registry.register_command(project_command::ProjectSlashCommand);
|
||||
slash_command_registry.register_command(search_command::SearchSlashCommand);
|
||||
|
||||
Self {
|
||||
workspace: workspace_handle,
|
||||
@ -1456,8 +1460,7 @@ enum ConversationEvent {
|
||||
updated: Vec<PendingSlashCommand>,
|
||||
},
|
||||
SlashCommandFinished {
|
||||
output_range: Range<language::Anchor>,
|
||||
render_placeholder: RenderFoldPlaceholder,
|
||||
sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
||||
},
|
||||
}
|
||||
|
||||
@ -1467,21 +1470,6 @@ struct Summary {
|
||||
done: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Eq, PartialEq, Hash)]
|
||||
pub struct SlashCommandInvocationId(usize);
|
||||
|
||||
impl SlashCommandInvocationId {
|
||||
fn post_inc(&mut self) -> Self {
|
||||
let id = *self;
|
||||
self.0 += 1;
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
struct SlashCommandInvocation {
|
||||
_pending_output: Task<Option<()>>,
|
||||
}
|
||||
|
||||
pub struct Conversation {
|
||||
id: Option<String>,
|
||||
buffer: Model<Buffer>,
|
||||
@ -1501,8 +1489,6 @@ pub struct Conversation {
|
||||
pending_edit_suggestion_parse: Option<Task<()>>,
|
||||
pending_save: Task<Result<()>>,
|
||||
path: Option<PathBuf>,
|
||||
invocations: HashMap<SlashCommandInvocationId, SlashCommandInvocation>,
|
||||
next_invocation_id: SlashCommandInvocationId,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
@ -1541,8 +1527,6 @@ impl Conversation {
|
||||
token_count: None,
|
||||
pending_token_count: Task::ready(None),
|
||||
pending_edit_suggestion_parse: None,
|
||||
next_invocation_id: SlashCommandInvocationId::default(),
|
||||
invocations: HashMap::default(),
|
||||
model,
|
||||
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
|
||||
pending_save: Task::ready(Ok(())),
|
||||
@ -1653,8 +1637,6 @@ impl Conversation {
|
||||
token_count: None,
|
||||
pending_edit_suggestion_parse: None,
|
||||
pending_token_count: Task::ready(None),
|
||||
next_invocation_id: SlashCommandInvocationId::default(),
|
||||
invocations: HashMap::default(),
|
||||
model,
|
||||
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
|
||||
pending_save: Task::ready(Ok(())),
|
||||
@ -1786,6 +1768,7 @@ impl Conversation {
|
||||
argument: argument.map(ToString::to_string),
|
||||
tooltip_text: command.tooltip_text().into(),
|
||||
source_range,
|
||||
status: PendingSlashCommandStatus::Idle,
|
||||
};
|
||||
updated.push(pending_command.clone());
|
||||
new_commands.push(pending_command);
|
||||
@ -1867,10 +1850,10 @@ impl Conversation {
|
||||
}
|
||||
|
||||
fn pending_command_for_position(
|
||||
&self,
|
||||
&mut self,
|
||||
position: language::Anchor,
|
||||
cx: &AppContext,
|
||||
) -> Option<&PendingSlashCommand> {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<&mut PendingSlashCommand> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let ix = self
|
||||
.pending_slash_commands
|
||||
@ -1884,54 +1867,72 @@ impl Conversation {
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
self.pending_slash_commands.get(ix)
|
||||
self.pending_slash_commands.get_mut(ix)
|
||||
}
|
||||
|
||||
fn insert_command_output(
|
||||
&mut self,
|
||||
invocation_id: SlashCommandInvocationId,
|
||||
command_range: Range<language::Anchor>,
|
||||
output: Task<Result<SlashCommandOutput>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.reparse_slash_commands(cx);
|
||||
|
||||
let insert_output_task = cx.spawn(|this, mut cx| {
|
||||
let command_range = command_range.clone();
|
||||
async move {
|
||||
let output = output.await?;
|
||||
let output = output.await;
|
||||
this.update(&mut cx, |this, cx| match output {
|
||||
Ok(output) => {
|
||||
let sections = this.buffer.update(cx, |buffer, cx| {
|
||||
let start = command_range.start.to_offset(buffer);
|
||||
let old_end = command_range.end.to_offset(buffer);
|
||||
let new_end = start + output.text.len();
|
||||
buffer.edit([(start..old_end, output.text)], None, cx);
|
||||
if buffer.chars_at(new_end).next() != Some('\n') {
|
||||
buffer.edit([(new_end..new_end, "\n")], None, cx);
|
||||
}
|
||||
|
||||
let mut text = output.text;
|
||||
LineEnding::normalize(&mut text);
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let output_range = this.buffer.update(cx, |buffer, cx| {
|
||||
let start = command_range.start.to_offset(buffer);
|
||||
let old_end = command_range.end.to_offset(buffer);
|
||||
let new_end = start + text.len();
|
||||
buffer.edit([(start..old_end, text)], None, cx);
|
||||
if buffer.chars_at(new_end).next() != Some('\n') {
|
||||
buffer.edit([(new_end..new_end, "\n")], None, cx);
|
||||
let mut sections = output
|
||||
.sections
|
||||
.into_iter()
|
||||
.map(|section| SlashCommandOutputSection {
|
||||
range: buffer.anchor_after(start + section.range.start)
|
||||
..buffer.anchor_before(start + section.range.end),
|
||||
render_placeholder: section.render_placeholder,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
||||
sections
|
||||
});
|
||||
cx.emit(ConversationEvent::SlashCommandFinished { sections });
|
||||
}
|
||||
Err(error) => {
|
||||
if let Some(pending_command) =
|
||||
this.pending_command_for_position(command_range.start, cx)
|
||||
{
|
||||
pending_command.status =
|
||||
PendingSlashCommandStatus::Error(error.to_string());
|
||||
cx.emit(ConversationEvent::PendingSlashCommandsUpdated {
|
||||
removed: vec![pending_command.source_range.clone()],
|
||||
updated: vec![pending_command.clone()],
|
||||
});
|
||||
}
|
||||
buffer.anchor_after(start)..buffer.anchor_before(new_end)
|
||||
});
|
||||
cx.emit(ConversationEvent::SlashCommandFinished {
|
||||
output_range,
|
||||
render_placeholder: output.render_placeholder,
|
||||
});
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
self.invocations.insert(
|
||||
invocation_id,
|
||||
SlashCommandInvocation {
|
||||
_pending_output: insert_output_task,
|
||||
},
|
||||
);
|
||||
if let Some(pending_command) = self.pending_command_for_position(command_range.start, cx) {
|
||||
pending_command.status = PendingSlashCommandStatus::Running {
|
||||
_task: insert_output_task.shared(),
|
||||
};
|
||||
cx.emit(ConversationEvent::PendingSlashCommandsUpdated {
|
||||
removed: vec![pending_command.source_range.clone()],
|
||||
updated: vec![pending_command.clone()],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn remaining_tokens(&self) -> Option<isize> {
|
||||
@ -2565,10 +2566,18 @@ fn parse_next_edit_suggestion(lines: &mut rope::Lines) -> Option<ParsedEditSugge
|
||||
struct PendingSlashCommand {
|
||||
name: String,
|
||||
argument: Option<String>,
|
||||
status: PendingSlashCommandStatus,
|
||||
source_range: Range<language::Anchor>,
|
||||
tooltip_text: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum PendingSlashCommandStatus {
|
||||
Idle,
|
||||
Running { _task: Shared<Task<()>> },
|
||||
Error(String),
|
||||
}
|
||||
|
||||
struct PendingCompletion {
|
||||
id: usize,
|
||||
_task: Task<()>,
|
||||
@ -2773,19 +2782,16 @@ impl ConversationEditor {
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<SlashCommandInvocationId> {
|
||||
let command = self.slash_command_registry.command(name)?;
|
||||
let lsp_adapter_delegate = self.lsp_adapter_delegate.clone()?;
|
||||
let argument = argument.map(ToString::to_string);
|
||||
let id = self.conversation.update(cx, |conversation, _| {
|
||||
conversation.next_invocation_id.post_inc()
|
||||
});
|
||||
let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
|
||||
self.conversation.update(cx, |conversation, cx| {
|
||||
conversation.insert_command_output(id, command_range, output, cx)
|
||||
});
|
||||
|
||||
Some(id)
|
||||
) {
|
||||
if let Some(command) = self.slash_command_registry.command(name) {
|
||||
if let Some(lsp_adapter_delegate) = self.lsp_adapter_delegate.clone() {
|
||||
let argument = argument.map(ToString::to_string);
|
||||
let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
|
||||
self.conversation.update(cx, |conversation, cx| {
|
||||
conversation.insert_command_output(command_range, output, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_conversation_event(
|
||||
@ -2901,6 +2907,7 @@ impl ConversationEditor {
|
||||
render_pending_slash_command_toggle(
|
||||
row,
|
||||
command.tooltip_text.clone(),
|
||||
command.status.clone(),
|
||||
confirm_command.clone(),
|
||||
)
|
||||
}
|
||||
@ -2935,39 +2942,38 @@ impl ConversationEditor {
|
||||
);
|
||||
})
|
||||
}
|
||||
ConversationEvent::SlashCommandFinished {
|
||||
output_range,
|
||||
render_placeholder,
|
||||
} => {
|
||||
ConversationEvent::SlashCommandFinished { sections } => {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let excerpt_id = *buffer.as_singleton().unwrap().0;
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, output_range.start)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, output_range.end)
|
||||
.unwrap();
|
||||
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||
|
||||
editor.insert_flaps(
|
||||
[Flap::new(
|
||||
let mut buffer_rows_to_fold = BTreeSet::new();
|
||||
let mut flaps = Vec::new();
|
||||
for section in sections {
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, section.range.start)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, section.range.end)
|
||||
.unwrap();
|
||||
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||
buffer_rows_to_fold.insert(buffer_row);
|
||||
flaps.push(Flap::new(
|
||||
start..end,
|
||||
FoldPlaceholder {
|
||||
render: Arc::new({
|
||||
let editor = cx.view().downgrade();
|
||||
let render_placeholder = render_placeholder.clone();
|
||||
let render_placeholder = section.render_placeholder.clone();
|
||||
move |fold_id, fold_range, cx| {
|
||||
let editor = editor.clone();
|
||||
let unfold = Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.unfold_ranges(
|
||||
[fold_range.start..fold_range.end],
|
||||
true,
|
||||
false,
|
||||
cx,
|
||||
let buffer_start = fold_range.start.to_point(
|
||||
&editor.buffer().read(cx).read(cx),
|
||||
);
|
||||
let buffer_row =
|
||||
MultiBufferRow(buffer_start.row);
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
@ -2979,10 +2985,14 @@ impl ConversationEditor {
|
||||
},
|
||||
render_slash_command_output_toggle,
|
||||
|_, _, _| Empty.into_any_element(),
|
||||
)],
|
||||
cx,
|
||||
);
|
||||
editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||
));
|
||||
}
|
||||
|
||||
editor.insert_flaps(flaps, cx);
|
||||
|
||||
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
|
||||
editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -3764,19 +3774,36 @@ fn render_slash_command_output_toggle(
|
||||
fn render_pending_slash_command_toggle(
|
||||
row: MultiBufferRow,
|
||||
tooltip_text: SharedString,
|
||||
status: PendingSlashCommandStatus,
|
||||
confirm_command: Arc<dyn Fn(&mut WindowContext)>,
|
||||
) -> AnyElement {
|
||||
IconButton::new(
|
||||
let mut icon = IconButton::new(
|
||||
("slash-command-output-fold-indicator", row.0),
|
||||
ui::IconName::TriangleRight,
|
||||
)
|
||||
.on_click(move |_e, cx| confirm_command(cx))
|
||||
.icon_color(ui::Color::Success)
|
||||
.icon_size(ui::IconSize::Small)
|
||||
.selected(true)
|
||||
.size(ui::ButtonSize::None)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx))
|
||||
.into_any_element()
|
||||
.size(ui::ButtonSize::None);
|
||||
|
||||
match status {
|
||||
PendingSlashCommandStatus::Idle => {
|
||||
icon = icon
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx));
|
||||
}
|
||||
PendingSlashCommandStatus::Running { .. } => {
|
||||
icon = icon
|
||||
.selected(true)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx));
|
||||
}
|
||||
PendingSlashCommandStatus::Error(error) => {
|
||||
icon = icon
|
||||
.icon_color(Color::Error)
|
||||
.tooltip(move |cx| Tooltip::text(format!("error: {error}"), cx));
|
||||
}
|
||||
}
|
||||
|
||||
icon.into_any_element()
|
||||
}
|
||||
|
||||
fn render_pending_slash_command_trailer(
|
||||
|
@ -20,6 +20,7 @@ pub mod active_command;
|
||||
pub mod file_command;
|
||||
pub mod project_command;
|
||||
pub mod prompt_command;
|
||||
pub mod search_command;
|
||||
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
editor: WeakView<ConversationEditor>,
|
||||
|
@ -1,5 +1,6 @@
|
||||
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use gpui::{AppContext, Entity, Task, WeakView};
|
||||
@ -96,16 +97,22 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
}
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await;
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text: text.await,
|
||||
render_placeholder: Arc::new(move |id, unfold, _| {
|
||||
FilePlaceholder {
|
||||
id,
|
||||
path: path.clone(),
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _| {
|
||||
FilePlaceholder {
|
||||
id,
|
||||
path: path.clone(),
|
||||
line_range: None,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
})
|
||||
})
|
||||
} else {
|
||||
|
@ -1,10 +1,12 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, Model, RenderOnce, SharedString, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use language::{LineEnding, LspAdapterDelegate};
|
||||
use project::{PathMatchCandidateSet, Project};
|
||||
use std::{
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
@ -128,7 +130,8 @@ impl SlashCommand for FileSlashCommand {
|
||||
let fs = project.fs().clone();
|
||||
let argument = argument.to_string();
|
||||
let text = cx.background_executor().spawn(async move {
|
||||
let content = fs.load(&abs_path).await?;
|
||||
let mut content = fs.load(&abs_path).await?;
|
||||
LineEnding::normalize(&mut content);
|
||||
let mut output = String::with_capacity(argument.len() + content.len() + 9);
|
||||
output.push_str("```");
|
||||
output.push_str(&argument);
|
||||
@ -142,16 +145,21 @@ impl SlashCommand for FileSlashCommand {
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
FilePlaceholder {
|
||||
path: Some(path.clone()),
|
||||
id,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
FilePlaceholder {
|
||||
path: Some(path.clone()),
|
||||
line_range: None,
|
||||
id,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -160,6 +168,7 @@ impl SlashCommand for FileSlashCommand {
|
||||
#[derive(IntoElement)]
|
||||
pub struct FilePlaceholder {
|
||||
pub path: Option<PathBuf>,
|
||||
pub line_range: Option<Range<u32>>,
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
}
|
||||
@ -178,6 +187,12 @@ impl RenderOnce for FilePlaceholder {
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::File))
|
||||
.child(Label::new(title))
|
||||
.when_some(self.line_range, |button, line_range| {
|
||||
button.child(Label::new(":")).child(Label::new(format!(
|
||||
"{}-{}",
|
||||
line_range.start, line_range.end
|
||||
)))
|
||||
})
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
@ -131,18 +132,21 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = output.await?;
|
||||
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::FileTree))
|
||||
.child(Label::new("Project"))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::FileTree))
|
||||
.child(Label::new("Project"))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
})
|
||||
})
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use crate::prompts::PromptLibrary;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
@ -94,17 +95,21 @@ impl SlashCommand for PromptSlashCommand {
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let prompt = prompt.await?;
|
||||
let range = 0..prompt.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text: prompt,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::Library))
|
||||
.child(Label::new(title.clone()))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::Library))
|
||||
.child(Label::new(title.clone()))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
164
crates/assistant/src/slash_command/search_command.rs
Normal file
164
crates/assistant/src/slash_command/search_command.rs
Normal file
@ -0,0 +1,164 @@
|
||||
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::{LineEnding, LspAdapterDelegate};
|
||||
use semantic_index::SemanticIndex;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex, Icon, IconName};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct SearchSlashCommand;
|
||||
|
||||
impl SlashCommand for SearchSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"search".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"semantically search files".into()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
"search".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
};
|
||||
let Some(argument) = argument else {
|
||||
return Task::ready(Err(anyhow::anyhow!("missing search query")));
|
||||
};
|
||||
if argument.is_empty() {
|
||||
return Task::ready(Err(anyhow::anyhow!("missing search query")));
|
||||
}
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let argument = argument.to_string();
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let project_index =
|
||||
cx.update_global(|index: &mut SemanticIndex, cx| index.project_index(project, cx));
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
let results = project_index
|
||||
.read_with(&cx, |project_index, cx| {
|
||||
project_index.search(argument.clone(), 5, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let mut loaded_results = Vec::new();
|
||||
for result in results {
|
||||
let (full_path, file_content) =
|
||||
result.worktree.read_with(&cx, |worktree, _cx| {
|
||||
let entry_abs_path = worktree.abs_path().join(&result.path);
|
||||
let mut entry_full_path = PathBuf::from(worktree.root_name());
|
||||
entry_full_path.push(&result.path);
|
||||
let file_content = async {
|
||||
let entry_abs_path = entry_abs_path;
|
||||
fs.load(&entry_abs_path).await
|
||||
};
|
||||
(entry_full_path, file_content)
|
||||
})?;
|
||||
if let Some(file_content) = file_content.await.log_err() {
|
||||
loaded_results.push((result, full_path, file_content));
|
||||
}
|
||||
}
|
||||
|
||||
let output = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let mut text = format!("Search results for {argument}:\n");
|
||||
let mut sections = Vec::new();
|
||||
for (result, full_path, file_content) in loaded_results {
|
||||
let range_start = result.range.start.min(file_content.len());
|
||||
let range_end = result.range.end.min(file_content.len());
|
||||
|
||||
let start_line =
|
||||
file_content[0..range_start].matches('\n').count() as u32 + 1;
|
||||
let end_line = file_content[0..range_end].matches('\n').count() as u32 + 1;
|
||||
let start_line_byte_offset = file_content[0..range_start]
|
||||
.rfind('\n')
|
||||
.map(|pos| pos + 1)
|
||||
.unwrap_or_default();
|
||||
let end_line_byte_offset = file_content[range_end..]
|
||||
.find('\n')
|
||||
.map(|pos| range_end + pos)
|
||||
.unwrap_or_else(|| file_content.len());
|
||||
|
||||
let section_start_ix = text.len();
|
||||
writeln!(
|
||||
text,
|
||||
"```{}:{}-{}",
|
||||
result.path.display(),
|
||||
start_line,
|
||||
end_line,
|
||||
)
|
||||
.unwrap();
|
||||
let mut excerpt =
|
||||
file_content[start_line_byte_offset..end_line_byte_offset].to_string();
|
||||
LineEnding::normalize(&mut excerpt);
|
||||
text.push_str(&excerpt);
|
||||
writeln!(text, "\n```\n").unwrap();
|
||||
let section_end_ix = text.len() - 1;
|
||||
|
||||
sections.push(SlashCommandOutputSection {
|
||||
range: section_start_ix..section_end_ix,
|
||||
render_placeholder: Arc::new(move |id, unfold, _| {
|
||||
FilePlaceholder {
|
||||
id,
|
||||
path: Some(full_path.clone()),
|
||||
line_range: Some(start_line..end_line),
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
let argument = SharedString::from(argument);
|
||||
sections.push(SlashCommandOutputSection {
|
||||
range: 0..text.len(),
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::MagnifyingGlass))
|
||||
.child(Label::new(argument.clone()))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
});
|
||||
|
||||
SlashCommandOutput { text, sections }
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(output)
|
||||
})
|
||||
}
|
||||
}
|
@ -4,7 +4,10 @@ use anyhow::Result;
|
||||
use gpui::{AnyElement, AppContext, ElementId, Task, WeakView, WindowContext};
|
||||
use language::LspAdapterDelegate;
|
||||
pub use slash_command_registry::*;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
@ -44,5 +47,11 @@ pub type RenderFoldPlaceholder = Arc<
|
||||
|
||||
pub struct SlashCommandOutput {
|
||||
pub text: String,
|
||||
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SlashCommandOutputSection<T> {
|
||||
pub range: Range<T>,
|
||||
pub render_placeholder: RenderFoldPlaceholder,
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::wasm_host::{WasmExtension, WasmHost};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use futures::FutureExt;
|
||||
use gpui::{AppContext, IntoElement, Task, WeakView, WindowContext};
|
||||
use language::LspAdapterDelegate;
|
||||
@ -49,7 +49,7 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let argument = argument.map(|arg| arg.to_string());
|
||||
let output = cx.background_executor().spawn(async move {
|
||||
let text = cx.background_executor().spawn(async move {
|
||||
let output = self
|
||||
.extension
|
||||
.call({
|
||||
@ -76,12 +76,16 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
output.ok_or_else(|| anyhow!("no output from command: {}", self.command.name))
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let output = output.await?;
|
||||
let text = text.await?;
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text: output,
|
||||
render_placeholder: Arc::new(|_, _, _| {
|
||||
"TODO: Extension command output".into_any_element()
|
||||
}),
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(|_, _, _| {
|
||||
"TODO: Extension command output".into_any_element()
|
||||
}),
|
||||
}],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ pub use language_registry::{
|
||||
pub use lsp::LanguageServerId;
|
||||
pub use outline::{Outline, OutlineItem};
|
||||
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer};
|
||||
pub use text::LineEnding;
|
||||
pub use text::{AnchorRangeExt, LineEnding};
|
||||
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
|
||||
|
||||
/// Initializes the `language` crate.
|
||||
|
@ -24,6 +24,10 @@ impl EmbeddingProvider for CloudEmbeddingProvider {
|
||||
// First, fetch any embeddings that are cached based on the requested texts' digests
|
||||
// Then compute any embeddings that are missing.
|
||||
async move {
|
||||
if !self.client.status().borrow().is_connected() {
|
||||
return Err(anyhow!("sign in required"));
|
||||
}
|
||||
|
||||
let cached_embeddings = self.client.request(proto::GetCachedEmbeddings {
|
||||
model: self.model.clone(),
|
||||
digests: texts
|
||||
|
@ -7,7 +7,7 @@ use chunking::{chunk_text, Chunk};
|
||||
use collections::{Bound, HashMap, HashSet};
|
||||
pub use embedding::*;
|
||||
use fs::Fs;
|
||||
use futures::stream::StreamExt;
|
||||
use futures::{future::Shared, stream::StreamExt, FutureExt};
|
||||
use futures_batch::ChunksTimeoutStreamExt;
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, BorrowAppContext, Context, Entity, EntityId, EventEmitter, Global,
|
||||
@ -115,9 +115,14 @@ pub struct ProjectIndex {
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum WorktreeIndexHandle {
|
||||
Loading { _task: Task<Result<()>> },
|
||||
Loaded { index: Model<WorktreeIndex> },
|
||||
Loading {
|
||||
index: Shared<Task<Result<Model<WorktreeIndex>, Arc<anyhow::Error>>>>,
|
||||
},
|
||||
Loaded {
|
||||
index: Model<WorktreeIndex>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProjectIndex {
|
||||
@ -213,26 +218,33 @@ impl ProjectIndex {
|
||||
);
|
||||
|
||||
let load_worktree = cx.spawn(|this, mut cx| async move {
|
||||
if let Some(worktree_index) = worktree_index.await.log_err() {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.worktree_indices.insert(
|
||||
worktree_id,
|
||||
WorktreeIndexHandle::Loaded {
|
||||
index: worktree_index,
|
||||
},
|
||||
);
|
||||
})?;
|
||||
} else {
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.worktree_indices.remove(&worktree_id)
|
||||
})?;
|
||||
}
|
||||
let result = match worktree_index.await {
|
||||
Ok(worktree_index) => {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.worktree_indices.insert(
|
||||
worktree_id,
|
||||
WorktreeIndexHandle::Loaded {
|
||||
index: worktree_index.clone(),
|
||||
},
|
||||
);
|
||||
})?;
|
||||
Ok(worktree_index)
|
||||
}
|
||||
Err(error) => {
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.worktree_indices.remove(&worktree_id)
|
||||
})?;
|
||||
Err(Arc::new(error))
|
||||
}
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| this.update_status(cx))
|
||||
this.update(&mut cx, |this, cx| this.update_status(cx))?;
|
||||
|
||||
result
|
||||
});
|
||||
|
||||
WorktreeIndexHandle::Loading {
|
||||
_task: load_worktree,
|
||||
index: load_worktree.shared(),
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -279,14 +291,22 @@ impl ProjectIndex {
|
||||
let (chunks_tx, chunks_rx) = channel::bounded(1024);
|
||||
let mut worktree_scan_tasks = Vec::new();
|
||||
for worktree_index in self.worktree_indices.values() {
|
||||
if let WorktreeIndexHandle::Loaded { index, .. } = worktree_index {
|
||||
let chunks_tx = chunks_tx.clone();
|
||||
index.read_with(cx, |index, cx| {
|
||||
let worktree_id = index.worktree.read(cx).id();
|
||||
let db_connection = index.db_connection.clone();
|
||||
let db = index.db;
|
||||
worktree_scan_tasks.push(cx.background_executor().spawn({
|
||||
async move {
|
||||
let worktree_index = worktree_index.clone();
|
||||
let chunks_tx = chunks_tx.clone();
|
||||
worktree_scan_tasks.push(cx.spawn(|cx| async move {
|
||||
let index = match worktree_index {
|
||||
WorktreeIndexHandle::Loading { index } => {
|
||||
index.clone().await.map_err(|error| anyhow!(error))?
|
||||
}
|
||||
WorktreeIndexHandle::Loaded { index } => index.clone(),
|
||||
};
|
||||
|
||||
index
|
||||
.read_with(&cx, |index, cx| {
|
||||
let worktree_id = index.worktree.read(cx).id();
|
||||
let db_connection = index.db_connection.clone();
|
||||
let db = index.db;
|
||||
cx.background_executor().spawn(async move {
|
||||
let txn = db_connection
|
||||
.read_txn()
|
||||
.context("failed to create read transaction")?;
|
||||
@ -300,10 +320,10 @@ impl ProjectIndex {
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
}));
|
||||
})
|
||||
}
|
||||
})
|
||||
})?
|
||||
.await
|
||||
}));
|
||||
}
|
||||
drop(chunks_tx);
|
||||
|
||||
@ -357,7 +377,9 @@ impl ProjectIndex {
|
||||
})
|
||||
.await;
|
||||
|
||||
futures::future::try_join_all(worktree_scan_tasks).await?;
|
||||
for scan_task in futures::future::join_all(worktree_scan_tasks).await {
|
||||
scan_task.log_err();
|
||||
}
|
||||
|
||||
project.read_with(&cx, |project, cx| {
|
||||
let mut search_results = Vec::with_capacity(results_by_worker.len() * limit);
|
||||
|
@ -1017,6 +1017,7 @@ mod tests {
|
||||
let workspace_1 = cx
|
||||
.read(|cx| cx.windows()[0].downcast::<Workspace>())
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
workspace_1
|
||||
.update(cx, |workspace, cx| {
|
||||
assert_eq!(workspace.worktrees(cx).count(), 2);
|
||||
|
Loading…
Reference in New Issue
Block a user