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