Cargo.lock 🔗
@@ -368,6 +368,7 @@ dependencies = [
"rope",
"schemars",
"search",
+ "semantic_index",
"serde",
"serde_json",
"settings",
Antonio Scandurra created
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
Cargo.lock | 1
crates/assistant/Cargo.toml | 1
crates/assistant/src/assistant.rs | 17
crates/assistant/src/assistant_panel.rs | 243 ++--
crates/assistant/src/slash_command.rs | 1
crates/assistant/src/slash_command/active_command.rs | 25
crates/assistant/src/slash_command/file_command.rs | 35
crates/assistant/src/slash_command/project_command.rs | 24
crates/assistant/src/slash_command/prompt_command.rs | 23
crates/assistant/src/slash_command/search_command.rs | 164 +++
crates/assistant_slash_command/src/assistant_slash_command.rs | 11
crates/extension/src/extension_slash_command.rs | 18
crates/language/src/language.rs | 2
crates/semantic_index/src/embedding/cloud.rs | 4
crates/semantic_index/src/semantic_index.rs | 86 +
crates/zed/src/zed.rs | 1
16 files changed, 469 insertions(+), 187 deletions(-)
@@ -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 mut text = output.text;
- LineEnding::normalize(&mut text);
- if !text.ends_with('\n') {
- text.push('\n');
- }
+ 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);
+ }
- 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()
+ }),
+ }],
})
})
}
@@ -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);