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:
Antonio Scandurra 2024-05-28 16:06:09 +02:00 committed by GitHub
parent 016a1444a7
commit 59662fbeb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 469 additions and 187 deletions

1
Cargo.lock generated
View File

@ -368,6 +368,7 @@ dependencies = [
"rope",
"schemars",
"search",
"semantic_index",
"serde",
"serde_json",
"settings",

View File

@ -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

View File

@ -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);

View File

@ -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(

View File

@ -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>,

View File

@ -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 {

View File

@ -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))
}
}

View File

@ -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()
}),
}],
})
})
});

View File

@ -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()
}),
}],
})
})
}

View 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)
})
}
}

View File

@ -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,
}

View File

@ -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()
}),
}],
})
})
}

View File

@ -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.

View File

@ -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

View File

@ -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);

View File

@ -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);