diff --git a/Cargo.lock b/Cargo.lock index 91d78e0b7e..f679ced758 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -368,6 +368,7 @@ dependencies = [ "rope", "schemars", "search", + "semantic_index", "serde", "serde_json", "settings", diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 1023296255..6e41f514f9 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -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 diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 6497da9b8a..bd039eeb74 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -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, 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); diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index dc604df07b..a5bcb2b5cb 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -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, }, SlashCommandFinished { - output_range: Range, - render_placeholder: RenderFoldPlaceholder, + sections: Vec>, }, } @@ -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>, -} - pub struct Conversation { id: Option, buffer: Model, @@ -1501,8 +1489,6 @@ pub struct Conversation { pending_edit_suggestion_parse: Option>, pending_save: Task>, path: Option, - invocations: HashMap, - next_invocation_id: SlashCommandInvocationId, _subscriptions: Vec, telemetry: Option>, slash_command_registry: Arc, @@ -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, + ) -> 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, output: Task>, cx: &mut ModelContext, ) { + 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::>(); + 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 { @@ -2565,10 +2566,18 @@ fn parse_next_edit_suggestion(lines: &mut rope::Lines) -> Option, + status: PendingSlashCommandStatus, source_range: Range, tooltip_text: SharedString, } +#[derive(Clone)] +enum PendingSlashCommandStatus { + Idle, + Running { _task: Shared> }, + Error(String), +} + struct PendingCompletion { id: usize, _task: Task<()>, @@ -2773,19 +2782,16 @@ impl ConversationEditor { argument: Option<&str>, workspace: WeakView, cx: &mut ViewContext, - ) -> Option { - 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, ) -> 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( diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index 58bcf900f6..e6c74c2530 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant/src/slash_command.rs @@ -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, diff --git a/crates/assistant/src/slash_command/active_command.rs b/crates/assistant/src/slash_command/active_command.rs index 47ff4f72e4..36465e92b8 100644 --- a/crates/assistant/src/slash_command/active_command.rs +++ b/crates/assistant/src/slash_command/active_command.rs @@ -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 { diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index a44f97f701..f55177eb1b 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -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, + pub line_range: Option>, pub id: ElementId, pub unfold: Arc, } @@ -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)) } } diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index 5aa5ebd607..0eba1e7793 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -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() + }), + }], }) }) }); diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index e4a0f11371..eaabf793c8 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -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() + }), + }], }) }) } diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs new file mode 100644 index 0000000000..267201bac5 --- /dev/null +++ b/crates/assistant/src/slash_command/search_command.rs @@ -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, + _cx: &mut AppContext, + ) -> Task>> { + Task::ready(Ok(Vec::new())) + } + + fn run( + self: Arc, + argument: Option<&str>, + workspace: WeakView, + _delegate: Arc, + cx: &mut WindowContext, + ) -> Task> { + 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) + }) + } +} diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 7f3a8df189..f3b039accf 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -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>, +} + +#[derive(Clone)] +pub struct SlashCommandOutputSection { + pub range: Range, pub render_placeholder: RenderFoldPlaceholder, } diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index e16b18cd31..72ca429ce7 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -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> { 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() + }), + }], }) }) } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 1b1967d08d..37839858fc 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -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. diff --git a/crates/semantic_index/src/embedding/cloud.rs b/crates/semantic_index/src/embedding/cloud.rs index ea09adea82..234fb20a2a 100644 --- a/crates/semantic_index/src/embedding/cloud.rs +++ b/crates/semantic_index/src/embedding/cloud.rs @@ -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 diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 94a2e22221..3d1bba04fb 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -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> }, - Loaded { index: Model }, + Loading { + index: Shared, Arc>>>, + }, + Loaded { + index: Model, + }, } 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); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f2eb0bccb1..9b5f3179da 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1017,6 +1017,7 @@ mod tests { let workspace_1 = cx .read(|cx| cx.windows()[0].downcast::()) .unwrap(); + cx.run_until_parked(); workspace_1 .update(cx, |workspace, cx| { assert_eq!(workspace.worktrees(cx).count(), 2);