Detailed changes
@@ -368,7 +368,6 @@ dependencies = [
"prompt_store",
"proto",
"rand 0.9.2",
- "ref-cast",
"release_channel",
"rope",
"rules_library",
@@ -394,7 +393,6 @@ dependencies = [
"ui_input",
"unindent",
"url",
- "urlencoding",
"util",
"watch",
"workspace",
@@ -607,7 +607,6 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
quote = "1.0.9"
rand = "0.9"
rayon = "1.8"
-ref-cast = "1.0.24"
regex = "1.5"
# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
@@ -239,13 +239,11 @@
"ctrl-alt-l": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "agent::ToggleModelSelector",
- "ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-alt-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl->": "agent::AddSelectionToThread",
- "ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
"super-ctrl-b": "agent::ToggleBurnMode",
@@ -322,17 +320,6 @@
"alt-enter": "editor::Newline"
}
},
- {
- "context": "ContextStrip",
- "bindings": {
- "up": "agent::FocusUp",
- "right": "agent::FocusRight",
- "left": "agent::FocusLeft",
- "down": "agent::FocusDown",
- "backspace": "agent::RemoveFocusedContext",
- "enter": "agent::AcceptSuggestedContext"
- }
- },
{
"context": "AcpThread > ModeSelector",
"bindings": {
@@ -824,8 +811,7 @@
"context": "PromptEditor",
"bindings": {
"ctrl-[": "agent::CyclePreviousInlineAssist",
- "ctrl-]": "agent::CycleNextInlineAssist",
- "ctrl-alt-e": "agent::RemoveAllContext"
+ "ctrl-]": "agent::CycleNextInlineAssist"
}
},
{
@@ -278,13 +278,11 @@
"cmd-alt-p": "agent::ManageProfiles",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "agent::ToggleModelSelector",
- "cmd-shift-a": "agent::ToggleContextPicker",
"cmd-shift-j": "agent::ToggleNavigationMenu",
"cmd-alt-m": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"cmd->": "agent::AddSelectionToThread",
- "cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-ctrl-b": "agent::ToggleBurnMode",
"cmd-shift-enter": "agent::ContinueThread",
@@ -365,18 +363,6 @@
"alt-enter": "editor::Newline"
}
},
- {
- "context": "ContextStrip",
- "use_key_equivalents": true,
- "bindings": {
- "up": "agent::FocusUp",
- "right": "agent::FocusRight",
- "left": "agent::FocusLeft",
- "down": "agent::FocusDown",
- "backspace": "agent::RemoveFocusedContext",
- "enter": "agent::AcceptSuggestedContext"
- }
- },
{
"context": "AgentConfiguration",
"bindings": {
@@ -889,9 +875,7 @@
"context": "PromptEditor",
"use_key_equivalents": true,
"bindings": {
- "cmd-shift-a": "agent::ToggleContextPicker",
"cmd-alt-/": "agent::ToggleModelSelector",
- "cmd-alt-e": "agent::RemoveAllContext",
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist"
}
@@ -240,13 +240,11 @@
"shift-alt-p": "agent::ManageProfiles",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-alt-/": "agent::ToggleModelSelector",
- "ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-alt-i": "agent::ToggleOptionsMenu",
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl-shift-.": "agent::AddSelectionToThread",
- "shift-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
"super-ctrl-b": "agent::ToggleBurnMode",
@@ -328,18 +326,6 @@
"alt-enter": "editor::Newline"
}
},
- {
- "context": "ContextStrip",
- "use_key_equivalents": true,
- "bindings": {
- "up": "agent::FocusUp",
- "right": "agent::FocusRight",
- "left": "agent::FocusLeft",
- "down": "agent::FocusDown",
- "backspace": "agent::RemoveFocusedContext",
- "enter": "agent::AcceptSuggestedContext"
- }
- },
{
"context": "AcpThread > ModeSelector",
"bindings": {
@@ -837,8 +823,7 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-[": "agent::CyclePreviousInlineAssist",
- "ctrl-]": "agent::CycleNextInlineAssist",
- "shift-alt-e": "agent::RemoveAllContext"
+ "ctrl-]": "agent::CycleNextInlineAssist"
}
},
{
@@ -69,7 +69,6 @@ postage.workspace = true
project.workspace = true
prompt_store.workspace = true
proto.workspace = true
-ref-cast.workspace = true
release_channel.workspace = true
rope.workspace = true
rules_library.workspace = true
@@ -93,7 +92,6 @@ time_format.workspace = true
ui.workspace = true
ui_input.workspace = true
url.workspace = true
-urlencoding.workspace = true
util.workspace = true
watch.workspace = true
workspace.workspace = true
@@ -1,4 +1,3 @@
-mod completion_provider;
mod entry_view_state;
mod message_editor;
mod mode_selector;
@@ -1,68 +1,45 @@
use crate::{
ChatWithFollow,
- acp::completion_provider::{ContextPickerCompletionProvider, SlashCommandCompletion},
- context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
+ completion_provider::{
+ PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextAction,
+ PromptContextType, SlashCommandCompletion,
+ },
+ mention_set::{
+ Mention, MentionImage, MentionSet, insert_crease_for_mention, paste_images_as_context,
+ },
};
-use acp_thread::{MentionUri, selection_name};
-use agent::{HistoryStore, outline};
+use acp_thread::MentionUri;
+use agent::HistoryStore;
use agent_client_protocol as acp;
-use agent_servers::{AgentServer, AgentServerDelegate};
use anyhow::{Result, anyhow};
-use assistant_slash_commands::codeblock_fence_for_path;
-use collections::{HashMap, HashSet};
+use collections::HashSet;
use editor::{
- Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
- EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay,
- MultiBuffer, MultiBufferOffset, ToOffset,
- actions::Paste,
- code_context_menus::CodeContextMenu,
- display_map::{Crease, CreaseId, FoldId},
- scroll::Autoscroll,
-};
-use futures::{
- FutureExt as _,
- future::{Shared, join_all},
+ Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
+ EditorEvent, EditorMode, EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, ToOffset,
+ actions::Paste, code_context_menus::CodeContextMenu, scroll::Autoscroll,
};
+use futures::{FutureExt as _, future::join_all};
use gpui::{
- Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId,
- EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
- Subscription, Task, TextStyle, WeakEntity, pulsating_between,
+ AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat, KeyContext,
+ SharedString, Subscription, Task, TextStyle, WeakEntity,
};
-use itertools::Either;
use language::{Buffer, Language, language_settings::InlayHintKind};
-use language_model::LanguageModelImage;
-use postage::stream::Stream as _;
-use project::{
- CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectItem, ProjectPath,
- Worktree,
-};
-use prompt_store::{PromptId, PromptStore};
+use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree};
+use prompt_store::PromptStore;
use rope::Point;
use settings::Settings;
-use std::{
- cell::RefCell,
- ffi::OsStr,
- fmt::Write,
- ops::{Range, RangeInclusive},
- path::{Path, PathBuf},
- rc::Rc,
- sync::Arc,
- time::Duration,
-};
-use text::OffsetRangeExt;
+use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc};
use theme::ThemeSettings;
-use ui::{ButtonLike, TintColor, Toggleable, prelude::*};
-use util::{ResultExt, debug_panic, rel_path::RelPath};
-use workspace::{CollaboratorId, Workspace, notifications::NotifyResultExt as _};
+use ui::prelude::*;
+use util::{ResultExt, debug_panic};
+use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::Chat;
pub struct MessageEditor {
- mention_set: MentionSet,
+ mention_set: Entity<MentionSet>,
editor: Entity<Editor>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
- history_store: Entity<HistoryStore>,
- prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
@@ -82,6 +59,41 @@ impl EventEmitter<MessageEditorEvent> for MessageEditor {}
const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
+impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
+ fn supports_images(&self, cx: &App) -> bool {
+ self.read(cx).prompt_capabilities.borrow().image
+ }
+
+ fn supported_modes(&self, cx: &App) -> Vec<PromptContextType> {
+ let mut supported = vec![PromptContextType::File, PromptContextType::Symbol];
+ if self.read(cx).prompt_capabilities.borrow().embedded_context {
+ supported.extend(&[
+ PromptContextType::Thread,
+ PromptContextType::Fetch,
+ PromptContextType::Rules,
+ ]);
+ }
+ supported
+ }
+
+ fn available_commands(&self, cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
+ self.read(cx)
+ .available_commands
+ .borrow()
+ .iter()
+ .map(|cmd| crate::completion_provider::AvailableCommand {
+ name: cmd.name.clone().into(),
+ description: cmd.description.clone().into(),
+ requires_argument: cmd.input.is_some(),
+ })
+ .collect()
+ }
+
+ fn confirm_command(&self, cx: &mut App) {
+ self.update(cx, |this, cx| this.send(cx));
+ }
+}
+
impl MessageEditor {
pub fn new(
workspace: WeakEntity<Workspace>,
@@ -103,15 +115,7 @@ impl MessageEditor {
},
None,
);
- let completion_provider = Rc::new(ContextPickerCompletionProvider::new(
- cx.weak_entity(),
- workspace.clone(),
- history_store.clone(),
- prompt_store.clone(),
- prompt_capabilities.clone(),
- available_commands.clone(),
- ));
- let mention_set = MentionSet::default();
+
let editor = cx.new(|cx| {
let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
@@ -121,7 +125,6 @@ impl MessageEditor {
editor.set_show_indent_guides(false, cx);
editor.set_soft_wrap();
editor.set_use_modal_editing(true);
- editor.set_completion_provider(Some(completion_provider.clone()));
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,
@@ -130,6 +133,26 @@ impl MessageEditor {
editor.register_addon(MessageEditorAddon::new());
editor
});
+ let mention_set = cx.new(|cx| {
+ MentionSet::new(
+ editor.clone(),
+ project.downgrade(),
+ history_store.clone(),
+ prompt_store.clone(),
+ window,
+ cx,
+ )
+ });
+ let completion_provider = Rc::new(PromptCompletionProvider::new(
+ cx.entity(),
+ mention_set.clone(),
+ history_store.clone(),
+ prompt_store.clone(),
+ workspace.clone(),
+ ));
+ editor.update(cx, |editor, _cx| {
+ editor.set_completion_provider(Some(completion_provider.clone()))
+ });
cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
cx.emit(MessageEditorEvent::Focus)
@@ -143,12 +166,12 @@ impl MessageEditor {
let mut has_hint = false;
let mut subscriptions = Vec::new();
- subscriptions.push(cx.subscribe_in(&editor, window, {
- move |this, editor, event, window, cx| {
+ subscriptions.push(cx.subscribe(&editor, {
+ move |this, editor, event, cx| {
if let EditorEvent::Edited { .. } = event
&& !editor.read(cx).read_only(cx)
{
- let snapshot = editor.update(cx, |editor, cx| {
+ editor.update(cx, |editor, cx| {
let new_hints = this
.command_hint(editor.buffer(), cx)
.into_iter()
@@ -164,11 +187,7 @@ impl MessageEditor {
cx,
);
has_hint = has_new_hint;
-
- editor.snapshot(window, cx)
});
- this.mention_set.remove_invalid(snapshot);
-
cx.notify();
}
}
@@ -179,8 +198,6 @@ impl MessageEditor {
project,
mention_set,
workspace,
- history_store,
- prompt_store,
prompt_capabilities,
available_commands,
agent_name,
@@ -238,6 +255,9 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
+ let Some(workspace) = self.workspace.upgrade() else {
+ return;
+ };
let uri = MentionUri::Thread {
id: thread.id.clone(),
name: thread.title.to_string(),
@@ -256,7 +276,21 @@ impl MessageEditor {
.text_anchor
});
- self.confirm_mention_completion(thread.title, start, content_len, uri, window, cx)
+ let supports_images = self.prompt_capabilities.borrow().image;
+
+ self.mention_set
+ .update(cx, |mention_set, cx| {
+ mention_set.confirm_mention_completion(
+ thread.title,
+ start,
+ content_len,
+ uri,
+ supports_images,
+ &workspace,
+ window,
+ cx,
+ )
+ })
.detach();
}
@@ -265,11 +299,6 @@ impl MessageEditor {
&self.editor
}
- #[cfg(test)]
- pub(crate) fn mention_set(&mut self) -> &mut MentionSet {
- &mut self.mention_set
- }
-
pub fn is_empty(&self, cx: &App) -> bool {
self.editor.read(cx).is_empty(cx)
}
@@ -283,388 +312,9 @@ impl MessageEditor {
.is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
}
- pub fn mentions(&self) -> HashSet<MentionUri> {
- self.mention_set
- .mentions
- .values()
- .map(|(uri, _)| uri.clone())
- .collect()
- }
-
- pub fn confirm_mention_completion(
- &mut self,
- crease_text: SharedString,
- start: text::Anchor,
- content_len: usize,
- mention_uri: MentionUri,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Task<()> {
- let snapshot = self
- .editor
- .update(cx, |editor, cx| editor.snapshot(window, cx));
- let Some(start_anchor) = snapshot.buffer_snapshot().as_singleton_anchor(start) else {
- return Task::ready(());
- };
- let excerpt_id = start_anchor.excerpt_id;
- let end_anchor = snapshot.buffer_snapshot().anchor_before(
- start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1usize,
- );
-
- let crease = if let MentionUri::File { abs_path } = &mention_uri
- && let Some(extension) = abs_path.extension()
- && let Some(extension) = extension.to_str()
- && Img::extensions().contains(&extension)
- && !extension.contains("svg")
- {
- let Some(project_path) = self
- .project
- .read(cx)
- .project_path_for_absolute_path(&abs_path, cx)
- else {
- log::error!("project path not found");
- return Task::ready(());
- };
- let image = self
- .project
- .update(cx, |project, cx| project.open_image(project_path, cx));
- let image = cx
- .spawn(async move |_, cx| {
- let image = image.await.map_err(|e| e.to_string())?;
- let image = image
- .update(cx, |image, _| image.image.clone())
- .map_err(|e| e.to_string())?;
- Ok(image)
- })
- .shared();
- insert_crease_for_mention(
- excerpt_id,
- start,
- content_len,
- mention_uri.name().into(),
- IconName::Image.path().into(),
- Some(image),
- self.editor.clone(),
- window,
- cx,
- )
- } else {
- insert_crease_for_mention(
- excerpt_id,
- start,
- content_len,
- crease_text,
- mention_uri.icon_path(cx),
- None,
- self.editor.clone(),
- window,
- cx,
- )
- };
- let Some((crease_id, tx)) = crease else {
- return Task::ready(());
- };
-
- let task = match mention_uri.clone() {
- MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx),
- MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
- MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
- MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
- MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx),
- MentionUri::Symbol {
- abs_path,
- line_range,
- ..
- } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
- MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
- MentionUri::PastedImage => {
- debug_panic!("pasted image URI should not be included in completions");
- Task::ready(Err(anyhow!(
- "pasted imaged URI should not be included in completions"
- )))
- }
- MentionUri::Selection { .. } => {
- debug_panic!("unexpected selection URI");
- Task::ready(Err(anyhow!("unexpected selection URI")))
- }
- };
- let task = cx
- .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
- .shared();
- self.mention_set
- .mentions
- .insert(crease_id, (mention_uri, task.clone()));
-
- // Notify the user if we failed to load the mentioned context
- cx.spawn_in(window, async move |this, cx| {
- let result = task.await.notify_async_err(cx);
- drop(tx);
- if result.is_none() {
- this.update(cx, |this, cx| {
- this.editor.update(cx, |editor, cx| {
- // Remove mention
- editor.edit([(start_anchor..end_anchor, "")], cx);
- });
- this.mention_set.mentions.remove(&crease_id);
- })
- .ok();
- }
- })
- }
-
- fn confirm_mention_for_file(
- &mut self,
- abs_path: PathBuf,
- cx: &mut Context<Self>,
- ) -> Task<Result<Mention>> {
- let Some(project_path) = self
- .project
- .read(cx)
- .project_path_for_absolute_path(&abs_path, cx)
- else {
- return Task::ready(Err(anyhow!("project path not found")));
- };
- let extension = abs_path
- .extension()
- .and_then(OsStr::to_str)
- .unwrap_or_default();
-
- if Img::extensions().contains(&extension) && !extension.contains("svg") {
- if !self.prompt_capabilities.borrow().image {
- return Task::ready(Err(anyhow!("This model does not support images yet")));
- }
- let task = self
- .project
- .update(cx, |project, cx| project.open_image(project_path, cx));
- return cx.spawn(async move |_, cx| {
- let image = task.await?;
- let image = image.update(cx, |image, _| image.image.clone())?;
- let format = image.format;
- let image = cx
- .update(|cx| LanguageModelImage::from_image(image, cx))?
- .await;
- if let Some(image) = image {
- Ok(Mention::Image(MentionImage {
- data: image.source,
- format,
- }))
- } else {
- Err(anyhow!("Failed to convert image"))
- }
- });
- }
-
- let buffer = self
- .project
- .update(cx, |project, cx| project.open_buffer(project_path, cx));
- cx.spawn(async move |_, cx| {
- let buffer = buffer.await?;
- let buffer_content = outline::get_buffer_content_or_outline(
- buffer.clone(),
- Some(&abs_path.to_string_lossy()),
- &cx,
- )
- .await?;
-
- Ok(Mention::Text {
- content: buffer_content.text,
- tracked_buffers: vec![buffer],
- })
- })
- }
-
- fn confirm_mention_for_fetch(
- &mut self,
- url: url::Url,
- cx: &mut Context<Self>,
- ) -> Task<Result<Mention>> {
- let http_client = match self
- .workspace
- .update(cx, |workspace, _| workspace.client().http_client())
- {
- Ok(http_client) => http_client,
- Err(e) => return Task::ready(Err(e)),
- };
- cx.background_executor().spawn(async move {
- let content = fetch_url_content(http_client, url.to_string()).await?;
- Ok(Mention::Text {
- content,
- tracked_buffers: Vec::new(),
- })
- })
- }
-
- fn confirm_mention_for_symbol(
- &mut self,
- abs_path: PathBuf,
- line_range: RangeInclusive<u32>,
- cx: &mut Context<Self>,
- ) -> Task<Result<Mention>> {
- let Some(project_path) = self
- .project
- .read(cx)
- .project_path_for_absolute_path(&abs_path, cx)
- else {
- return Task::ready(Err(anyhow!("project path not found")));
- };
- let buffer = self
- .project
- .update(cx, |project, cx| project.open_buffer(project_path, cx));
- cx.spawn(async move |_, cx| {
- let buffer = buffer.await?;
- let mention = buffer.update(cx, |buffer, cx| {
- let start = Point::new(*line_range.start(), 0).min(buffer.max_point());
- let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point());
- let content = buffer.text_for_range(start..end).collect();
- Mention::Text {
- content,
- tracked_buffers: vec![cx.entity()],
- }
- })?;
- anyhow::Ok(mention)
- })
- }
-
- fn confirm_mention_for_rule(
- &mut self,
- id: PromptId,
- cx: &mut Context<Self>,
- ) -> Task<Result<Mention>> {
- let Some(prompt_store) = self.prompt_store.clone() else {
- return Task::ready(Err(anyhow!("missing prompt store")));
- };
- let prompt = prompt_store.read(cx).load(id, cx);
- cx.spawn(async move |_, _| {
- let prompt = prompt.await?;
- Ok(Mention::Text {
- content: prompt,
- tracked_buffers: Vec::new(),
- })
- })
- }
-
- pub fn confirm_mention_for_selection(
- &mut self,
- source_range: Range<text::Anchor>,
- selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
- let Some(start) = snapshot.as_singleton_anchor(source_range.start) else {
- return;
- };
-
- let offset = start.to_offset(&snapshot);
-
- for (buffer, selection_range, range_to_fold) in selections {
- let range = snapshot.anchor_after(offset + range_to_fold.start)
- ..snapshot.anchor_after(offset + range_to_fold.end);
-
- let abs_path = buffer
- .read(cx)
- .project_path(cx)
- .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx));
- let snapshot = buffer.read(cx).snapshot();
-
- let text = snapshot
- .text_for_range(selection_range.clone())
- .collect::<String>();
- let point_range = selection_range.to_point(&snapshot);
- let line_range = point_range.start.row..=point_range.end.row;
-
- let uri = MentionUri::Selection {
- abs_path: abs_path.clone(),
- line_range: line_range.clone(),
- };
- let crease = crate::context_picker::crease_for_mention(
- selection_name(abs_path.as_deref(), &line_range).into(),
- uri.icon_path(cx),
- range,
- self.editor.downgrade(),
- );
-
- let crease_id = self.editor.update(cx, |editor, cx| {
- let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
- editor.fold_creases(vec![crease], false, window, cx);
- crease_ids.first().copied().unwrap()
- });
-
- self.mention_set.mentions.insert(
- crease_id,
- (
- uri,
- Task::ready(Ok(Mention::Text {
- content: text,
- tracked_buffers: vec![buffer],
- }))
- .shared(),
- ),
- );
- }
-
- // Take this explanation with a grain of salt but, with creases being
- // inserted, GPUI's recomputes the editor layout in the next frames, so
- // directly calling `editor.request_autoscroll` wouldn't work as
- // expected. We're leveraging `cx.on_next_frame` to wait 2 frames and
- // ensure that the layout has been recalculated so that the autoscroll
- // request actually shows the cursor's new position.
- let editor = self.editor.clone();
- cx.on_next_frame(window, move |_, window, cx| {
- cx.on_next_frame(window, move |_, _, cx| {
- editor.update(cx, |editor, cx| {
- editor.request_autoscroll(Autoscroll::fit(), cx)
- });
- });
- });
- }
-
- fn confirm_mention_for_thread(
- &mut self,
- id: acp::SessionId,
- cx: &mut Context<Self>,
- ) -> Task<Result<Mention>> {
- let server = Rc::new(agent::NativeAgentServer::new(
- self.project.read(cx).fs().clone(),
- self.history_store.clone(),
- ));
- let delegate = AgentServerDelegate::new(
- self.project.read(cx).agent_server_store().clone(),
- self.project.clone(),
- None,
- None,
- );
- let connection = server.connect(None, delegate, cx);
- cx.spawn(async move |_, cx| {
- let (agent, _) = connection.await?;
- let agent = agent.downcast::<agent::NativeAgentConnection>().unwrap();
- let summary = agent
- .0
- .update(cx, |agent, cx| agent.thread_summary(id, cx))?
- .await?;
- anyhow::Ok(Mention::Text {
- content: summary.to_string(),
- tracked_buffers: Vec::new(),
- })
- })
- }
-
- fn confirm_mention_for_text_thread(
- &mut self,
- path: PathBuf,
- cx: &mut Context<Self>,
- ) -> Task<Result<Mention>> {
- let text_thread_task = self.history_store.update(cx, |store, cx| {
- store.load_text_thread(path.as_path().into(), cx)
- });
- cx.spawn(async move |_, cx| {
- let text_thread = text_thread_task.await?;
- let xml = text_thread.update(cx, |text_thread, cx| text_thread.to_xml(cx))?;
- Ok(Mention::Text {
- content: xml,
- tracked_buffers: Vec::new(),
- })
- })
+ #[cfg(test)]
+ pub fn mention_set(&self) -> &Entity<MentionSet> {
+ &self.mention_set
}
fn validate_slash_commands(
@@ -716,7 +366,7 @@ impl MessageEditor {
let contents = self
.mention_set
- .contents(full_mention_content, self.project.clone(), cx);
+ .update(cx, |store, cx| store.contents(full_mention_content, cx));
let editor = self.editor.clone();
let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context;
@@ -828,10 +478,12 @@ impl MessageEditor {
self.editor.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.remove_creases(
- self.mention_set
- .mentions
- .drain()
- .map(|(crease_id, _)| crease_id),
+ self.mention_set.update(cx, |mention_set, _cx| {
+ mention_set
+ .remove_all()
+ .map(|(crease_id, _)| crease_id)
+ .collect::<Vec<_>>()
+ }),
cx,
)
});
@@ -910,153 +562,12 @@ impl MessageEditor {
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
- if !self.prompt_capabilities.borrow().image {
- return;
+ if self.prompt_capabilities.borrow().image
+ && let Some(task) =
+ paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
+ {
+ task.detach();
}
- let Some(clipboard) = cx.read_from_clipboard() else {
- return;
- };
- cx.spawn_in(window, async move |this, cx| {
- use itertools::Itertools;
- let (mut images, paths) = clipboard
- .into_entries()
- .filter_map(|entry| match entry {
- ClipboardEntry::Image(image) => Some(Either::Left(image)),
- ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
- _ => None,
- })
- .partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
-
- if !paths.is_empty() {
- images.extend(
- cx.background_spawn(async move {
- let mut images = vec![];
- for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) {
- let Ok(content) = async_fs::read(path).await else {
- continue;
- };
- let Ok(format) = image::guess_format(&content) else {
- continue;
- };
- images.push(gpui::Image::from_bytes(
- match format {
- image::ImageFormat::Png => gpui::ImageFormat::Png,
- image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
- image::ImageFormat::WebP => gpui::ImageFormat::Webp,
- image::ImageFormat::Gif => gpui::ImageFormat::Gif,
- image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
- image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
- image::ImageFormat::Ico => gpui::ImageFormat::Ico,
- _ => continue,
- },
- content,
- ));
- }
- images
- })
- .await,
- );
- }
-
- if images.is_empty() {
- return;
- }
-
- let replacement_text = MentionUri::PastedImage.as_link().to_string();
- let Ok(editor) = this.update(cx, |this, cx| {
- cx.stop_propagation();
- this.editor.clone()
- }) else {
- return;
- };
- for image in images {
- let Ok((excerpt_id, text_anchor, multibuffer_anchor)) =
- editor.update_in(cx, |message_editor, window, cx| {
- let snapshot = message_editor.snapshot(window, cx);
- let (excerpt_id, _, buffer_snapshot) =
- snapshot.buffer_snapshot().as_singleton().unwrap();
-
- let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
- let multibuffer_anchor = snapshot
- .buffer_snapshot()
- .anchor_in_excerpt(*excerpt_id, text_anchor);
- message_editor.edit(
- [(
- multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
- format!("{replacement_text} "),
- )],
- cx,
- );
- (*excerpt_id, text_anchor, multibuffer_anchor)
- })
- else {
- break;
- };
-
- let content_len = replacement_text.len();
- let Some(start_anchor) = multibuffer_anchor else {
- continue;
- };
- let Ok(end_anchor) = editor.update(cx, |editor, cx| {
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
- }) else {
- continue;
- };
- let image = Arc::new(image);
- let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
- insert_crease_for_mention(
- excerpt_id,
- text_anchor,
- content_len,
- MentionUri::PastedImage.name().into(),
- IconName::Image.path().into(),
- Some(Task::ready(Ok(image.clone())).shared()),
- editor.clone(),
- window,
- cx,
- )
- }) else {
- continue;
- };
- let task = cx
- .spawn(async move |cx| {
- let format = image.format;
- let image = cx
- .update(|_, cx| LanguageModelImage::from_image(image, cx))
- .map_err(|e| e.to_string())?
- .await;
- drop(tx);
- if let Some(image) = image {
- Ok(Mention::Image(MentionImage {
- data: image.source,
- format,
- }))
- } else {
- Err("Failed to convert image".into())
- }
- })
- .shared();
-
- this.update(cx, |this, _| {
- this.mention_set
- .mentions
- .insert(crease_id, (MentionUri::PastedImage, task.clone()))
- })
- .ok();
-
- if task.await.notify_async_err(cx).is_none() {
- this.update(cx, |this, cx| {
- this.editor.update(cx, |editor, cx| {
- editor.edit([(start_anchor..end_anchor, "")], cx);
- });
- this.mention_set.mentions.remove(&crease_id);
- })
- .ok();
- }
- }
- })
- .detach();
}
pub fn insert_dragged_files(
@@ -1066,6 +577,9 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
+ let Some(workspace) = self.workspace.upgrade() else {
+ return;
+ };
let path_style = self.project.read(cx).path_style(cx);
let buffer = self.editor.read(cx).buffer().clone();
let Some(buffer) = buffer.read(cx).as_singleton() else {
@@ -1080,12 +594,11 @@ impl MessageEditor {
continue;
};
let abs_path = worktree.read(cx).absolutize(&path.path);
- let (file_name, _) =
- crate::context_picker::file_context_picker::extract_file_name_and_directory(
- &path.path,
- worktree.read(cx).root_name(),
- path_style,
- );
+ let (file_name, _) = crate::completion_provider::extract_file_name_and_directory(
+ &path.path,
+ worktree.read(cx).root_name(),
+ path_style,
+ );
let uri = if entry.is_dir() {
MentionUri::Directory { abs_path }
@@ -1107,14 +620,19 @@ impl MessageEditor {
cx,
);
});
- tasks.push(self.confirm_mention_completion(
- file_name,
- anchor,
- content_len,
- uri,
- window,
- cx,
- ));
+ let supports_images = self.prompt_capabilities.borrow().image;
+ tasks.push(self.mention_set.update(cx, |mention_set, cx| {
+ mention_set.confirm_mention_completion(
+ file_name,
+ anchor,
+ content_len,
+ uri,
+ supports_images,
+ &workspace,
+ window,
+ cx,
+ )
+ }));
}
cx.spawn(async move |_, _| {
join_all(tasks).await;
@@ -1137,13 +655,15 @@ impl MessageEditor {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
- let Some(completion) = ContextPickerCompletionProvider::completion_for_action(
- ContextPickerAction::AddSelections,
- anchor..anchor,
- cx.weak_entity(),
- &workspace,
- cx,
- ) else {
+ let Some(completion) =
+ PromptCompletionProvider::<Entity<MessageEditor>>::completion_for_action(
+ PromptContextAction::AddSelections,
+ anchor..anchor,
+ self.mention_set.downgrade(),
+ &workspace,
+ cx,
+ )
+ else {
return;
};
@@ -1274,10 +794,13 @@ impl MessageEditor {
};
drop(tx);
- self.mention_set.mentions.insert(
- crease_id,
- (mention_uri.clone(), Task::ready(Ok(mention)).shared()),
- );
+ self.mention_set.update(cx, |mention_set, _cx| {
+ mention_set.insert_mention(
+ crease_id,
+ mention_uri.clone(),
+ Task::ready(Ok(mention)).shared(),
+ )
+ });
}
cx.notify();
}
@@ -1305,111 +828,6 @@ impl MessageEditor {
}
}
-fn full_mention_for_directory(
- project: &Entity<Project>,
- abs_path: &Path,
- cx: &mut App,
-) -> Task<Result<Mention>> {
- fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
- let mut files = Vec::new();
-
- for entry in worktree.child_entries(path) {
- if entry.is_dir() {
- files.extend(collect_files_in_path(worktree, &entry.path));
- } else if entry.is_file() {
- files.push((
- entry.path.clone(),
- worktree
- .full_path(&entry.path)
- .to_string_lossy()
- .to_string(),
- ));
- }
- }
-
- files
- }
-
- let Some(project_path) = project
- .read(cx)
- .project_path_for_absolute_path(&abs_path, cx)
- else {
- return Task::ready(Err(anyhow!("project path not found")));
- };
- let Some(entry) = project.read(cx).entry_for_path(&project_path, cx) else {
- return Task::ready(Err(anyhow!("project entry not found")));
- };
- let directory_path = entry.path.clone();
- let worktree_id = project_path.worktree_id;
- let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
- return Task::ready(Err(anyhow!("worktree not found")));
- };
- let project = project.clone();
- cx.spawn(async move |cx| {
- let file_paths = worktree.read_with(cx, |worktree, _cx| {
- collect_files_in_path(worktree, &directory_path)
- })?;
- let descendants_future = cx.update(|cx| {
- join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
- let rel_path = worktree_path
- .strip_prefix(&directory_path)
- .log_err()
- .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
-
- let open_task = project.update(cx, |project, cx| {
- project.buffer_store().update(cx, |buffer_store, cx| {
- let project_path = ProjectPath {
- worktree_id,
- path: worktree_path,
- };
- buffer_store.open_buffer(project_path, cx)
- })
- });
-
- cx.spawn(async move |cx| {
- let buffer = open_task.await.log_err()?;
- let buffer_content = outline::get_buffer_content_or_outline(
- buffer.clone(),
- Some(&full_path),
- &cx,
- )
- .await
- .ok()?;
-
- Some((rel_path, full_path, buffer_content.text, buffer))
- })
- }))
- })?;
-
- let contents = cx
- .background_spawn(async move {
- let (contents, tracked_buffers) = descendants_future
- .await
- .into_iter()
- .flatten()
- .map(|(rel_path, full_path, rope, buffer)| {
- ((rel_path, full_path, rope), buffer)
- })
- .unzip();
- Mention::Text {
- content: render_directory_contents(contents),
- tracked_buffers,
- }
- })
- .await;
- anyhow::Ok(contents)
- })
-}
-
-fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
- let mut output = String::new();
- for (_relative_path, full_path, content) in entries {
- let fence = codeblock_fence_for_path(Some(&full_path), None);
- write!(output, "\n{fence}\n{content}\n```").unwrap();
- }
- output
-}
-
impl Focusable for MessageEditor {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.editor.focus_handle(cx)
@@ -17,6 +17,7 @@ use settings::{
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
+use crate::ManageProfiles;
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
use crate::{
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
@@ -34,7 +35,6 @@ use crate::{
acp::{AcpThreadHistory, ThreadHistoryEvent},
};
use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
-use crate::{ManageProfiles, context_store::ContextStore};
use agent_settings::AgentSettings;
use ai_onboarding::AgentPanelOnboarding;
use anyhow::{Result, anyhow};
@@ -431,7 +431,6 @@ pub struct AgentPanel {
text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
context_server_registry: Entity<ContextServerRegistry>,
- inline_assist_context_store: Entity<ContextStore>,
configuration: Option<Entity<AgentConfiguration>>,
configuration_subscription: Option<Subscription>,
active_view: ActiveView,
@@ -543,7 +542,6 @@ impl AgentPanel {
let client = workspace.client().clone();
let workspace = workspace.weak_handle();
- let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
@@ -680,7 +678,6 @@ impl AgentPanel {
configuration: None,
configuration_subscription: None,
context_server_registry,
- inline_assist_context_store,
previous_view: None,
new_thread_menu_handle: PopoverMenuHandle::default(),
agent_panel_menu_handle: PopoverMenuHandle::default(),
@@ -721,10 +718,6 @@ impl AgentPanel {
&self.prompt_store
}
- pub(crate) fn inline_assist_context_store(&self) -> &Entity<ContextStore> {
- &self.inline_assist_context_store
- }
-
pub(crate) fn thread_store(&self) -> &Entity<HistoryStore> {
&self.history_store
}
@@ -2664,23 +2657,19 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
cx: &mut Context<RulesLibrary>,
) {
InlineAssistant::update_global(cx, |assistant, cx| {
- let Some(project) = self
- .workspace
- .upgrade()
- .map(|workspace| workspace.read(cx).project().downgrade())
- else {
+ let Some(workspace) = self.workspace.upgrade() else {
+ return;
+ };
+ let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
return;
};
- let prompt_store = None;
- let thread_store = None;
- let context_store = cx.new(|_| ContextStore::new(project.clone()));
+ let project = workspace.read(cx).project().downgrade();
assistant.assist(
prompt_editor,
self.workspace.clone(),
- context_store,
project,
- prompt_store,
- thread_store,
+ panel.read(cx).thread_store().clone(),
+ None,
initial_prompt,
window,
cx,
@@ -4,14 +4,13 @@ mod agent_diff;
mod agent_model_selector;
mod agent_panel;
mod buffer_codegen;
+mod completion_provider;
mod context;
-mod context_picker;
mod context_server_configuration;
-mod context_store;
-mod context_strip;
mod inline_assistant;
mod inline_prompt_editor;
mod language_model_selector;
+mod mention_set;
mod profile_selector;
mod slash_command;
mod slash_command_picker;
@@ -35,7 +34,7 @@ use language::{
language_settings::{AllLanguageSettings, EditPredictionProvider},
};
use language_model::{
- ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
+ ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
};
use project::DisableAiSettings;
use prompt_store::PromptBuilder;
@@ -56,8 +55,6 @@ actions!(
[
/// Creates a new text-based conversation thread.
NewTextThread,
- /// Toggles the context picker interface for adding files, symbols, or other context.
- ToggleContextPicker,
/// Toggles the menu to create new agent threads.
ToggleNewThreadMenu,
/// Toggles the navigation menu for switching between threads and views.
@@ -70,8 +67,6 @@ actions!(
ToggleProfileSelector,
/// Cycles through available session modes.
CycleModeSelector,
- /// Removes all added context from the current conversation.
- RemoveAllContext,
/// Expands the message editor to full size.
ExpandMessageEditor,
/// Opens the conversation history view.
@@ -94,10 +89,6 @@ actions!(
FocusLeft,
/// Moves focus right in the interface.
FocusRight,
- /// Removes the currently focused context item.
- RemoveFocusedContext,
- /// Accepts the suggested context item.
- AcceptSuggestedContext,
/// Opens the active thread as a markdown file.
OpenActiveThreadAsMarkdown,
/// Opens the agent diff view to review changes.
@@ -220,11 +211,6 @@ impl ModelUsageContext {
}
}
}
-
- pub fn language_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
- self.configured_model(cx)
- .map(|configured_model| configured_model.model)
- }
}
/// Initializes the `agent` crate.
@@ -1,6 +1,4 @@
-use crate::{
- context::load_context, context_store::ContextStore, inline_prompt_editor::CodegenStatus,
-};
+use crate::{context::LoadedContext, inline_prompt_editor::CodegenStatus};
use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
@@ -8,9 +6,12 @@ use cloud_llm_client::CompletionIntent;
use collections::HashSet;
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
use futures::{
- SinkExt, Stream, StreamExt, TryStreamExt as _, channel::mpsc, future::LocalBoxFuture, join,
+ SinkExt, Stream, StreamExt, TryStreamExt as _,
+ channel::mpsc,
+ future::{LocalBoxFuture, Shared},
+ join,
};
-use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task, WeakEntity};
+use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
@@ -18,8 +19,7 @@ use language_model::{
};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
-use project::Project;
-use prompt_store::{PromptBuilder, PromptStore};
+use prompt_store::PromptBuilder;
use rope::Rope;
use smol::future::FutureExt;
use std::{
@@ -43,9 +43,6 @@ pub struct BufferCodegen {
buffer: Entity<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
- context_store: Entity<ContextStore>,
- project: WeakEntity<Project>,
- prompt_store: Option<Entity<PromptStore>>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
pub is_insertion: bool,
@@ -56,9 +53,6 @@ impl BufferCodegen {
buffer: Entity<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
- context_store: Entity<ContextStore>,
- project: WeakEntity<Project>,
- prompt_store: Option<Entity<PromptStore>>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
cx: &mut Context<Self>,
@@ -68,9 +62,6 @@ impl BufferCodegen {
buffer.clone(),
range.clone(),
false,
- Some(context_store.clone()),
- project.clone(),
- prompt_store.clone(),
Some(telemetry.clone()),
builder.clone(),
cx,
@@ -85,9 +76,6 @@ impl BufferCodegen {
buffer,
range,
initial_transaction_id,
- context_store,
- project,
- prompt_store,
telemetry,
builder,
};
@@ -148,6 +136,7 @@ impl BufferCodegen {
&mut self,
primary_model: Arc<dyn LanguageModel>,
user_prompt: String,
+ context_task: Shared<Task<Option<LoadedContext>>>,
cx: &mut Context<Self>,
) -> Result<()> {
let alternative_models = LanguageModelRegistry::read_global(cx)
@@ -165,9 +154,6 @@ impl BufferCodegen {
self.buffer.clone(),
self.range.clone(),
false,
- Some(self.context_store.clone()),
- self.project.clone(),
- self.prompt_store.clone(),
Some(self.telemetry.clone()),
self.builder.clone(),
cx,
@@ -180,7 +166,7 @@ impl BufferCodegen {
.zip(&self.alternatives)
{
alternative.update(cx, |alternative, cx| {
- alternative.start(user_prompt.clone(), model.clone(), cx)
+ alternative.start(user_prompt.clone(), context_task.clone(), model.clone(), cx)
})?;
}
@@ -243,9 +229,6 @@ pub struct CodegenAlternative {
status: CodegenStatus,
generation: Task<()>,
diff: Diff,
- context_store: Option<Entity<ContextStore>>,
- project: WeakEntity<Project>,
- prompt_store: Option<Entity<PromptStore>>,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
builder: Arc<PromptBuilder>,
@@ -264,9 +247,6 @@ impl CodegenAlternative {
buffer: Entity<MultiBuffer>,
range: Range<Anchor>,
active: bool,
- context_store: Option<Entity<ContextStore>>,
- project: WeakEntity<Project>,
- prompt_store: Option<Entity<PromptStore>>,
telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
cx: &mut Context<Self>,
@@ -307,9 +287,6 @@ impl CodegenAlternative {
status: CodegenStatus::Idle,
generation: Task::ready(()),
diff: Diff::default(),
- context_store,
- project,
- prompt_store,
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
builder,
@@ -366,6 +343,7 @@ impl CodegenAlternative {
pub fn start(
&mut self,
user_prompt: String,
+ context_task: Shared<Task<Option<LoadedContext>>>,
model: Arc<dyn LanguageModel>,
cx: &mut Context<Self>,
) -> Result<()> {
@@ -384,7 +362,7 @@ impl CodegenAlternative {
if user_prompt.trim().to_lowercase() == "delete" {
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
} else {
- let request = self.build_request(&model, user_prompt, cx)?;
+ let request = self.build_request(&model, user_prompt, context_task, cx)?;
cx.spawn(async move |_, cx| {
Ok(model.stream_completion_text(request.await, cx).await?)
})
@@ -398,6 +376,7 @@ impl CodegenAlternative {
&self,
model: &Arc<dyn LanguageModel>,
user_prompt: String,
+ context_task: Shared<Task<Option<LoadedContext>>>,
cx: &mut App,
) -> Result<Task<LanguageModelRequest>> {
let buffer = self.buffer.read(cx).snapshot(cx);
@@ -437,19 +416,6 @@ impl CodegenAlternative {
)
.context("generating content prompt")?;
- let context_task = self.context_store.as_ref().and_then(|context_store| {
- if let Some(project) = self.project.upgrade() {
- let context = context_store
- .read(cx)
- .context()
- .cloned()
- .collect::<Vec<_>>();
- Some(load_context(context, &project, &self.prompt_store, cx))
- } else {
- None
- }
- });
-
let temperature = AgentSettings::temperature_for_model(model, cx);
Ok(cx.spawn(async move |_cx| {
@@ -459,10 +425,8 @@ impl CodegenAlternative {
cache: false,
};
- if let Some(context_task) = context_task {
- context_task
- .await
- .add_to_request_message(&mut request_message);
+ if let Some(context) = context_task.await {
+ context.add_to_request_message(&mut request_message);
}
request_message.content.push(prompt.into());
@@ -1088,7 +1052,6 @@ impl Diff {
#[cfg(test)]
mod tests {
use super::*;
- use fs::FakeFs;
use futures::{
Stream,
stream::{self},
@@ -1120,17 +1083,12 @@ mod tests {
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let fs = FakeFs::new(cx.executor());
- let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
None,
- project.downgrade(),
- None,
- None,
prompt_builder,
cx,
)
@@ -1187,17 +1145,12 @@ mod tests {
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let fs = FakeFs::new(cx.executor());
- let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
None,
- project.downgrade(),
- None,
- None,
prompt_builder,
cx,
)
@@ -1256,17 +1209,12 @@ mod tests {
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let fs = FakeFs::new(cx.executor());
- let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
None,
- project.downgrade(),
- None,
- None,
prompt_builder,
cx,
)
@@ -1325,17 +1273,12 @@ mod tests {
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let fs = FakeFs::new(cx.executor());
- let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
None,
- project.downgrade(),
- None,
- None,
prompt_builder,
cx,
)
@@ -1382,17 +1325,12 @@ mod tests {
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let fs = FakeFs::new(cx.executor());
- let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
false,
None,
- project.downgrade(),
- None,
- None,
prompt_builder,
cx,
)
@@ -1,41 +1,130 @@
-use std::cell::RefCell;
+use std::cmp::Reverse;
use std::ops::Range;
use std::path::PathBuf;
-use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use acp_thread::MentionUri;
use agent::{HistoryEntry, HistoryStore};
-use agent_client_protocol as acp;
use anyhow::Result;
use editor::{CompletionProvider, Editor, ExcerptId};
-use fuzzy::{StringMatch, StringMatchCandidate};
+use fuzzy::{PathMatch, StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
use lsp::CompletionContext;
+use ordered_float::OrderedFloat;
use project::lsp_store::{CompletionDocumentation, SymbolLocation};
use project::{
- Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
- ProjectPath, Symbol, WorktreeId,
+ Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse,
+ PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId,
};
-use prompt_store::PromptStore;
+use prompt_store::{PromptId, PromptStore, UserPromptId};
use rope::Point;
use text::{Anchor, ToPoint as _};
use ui::prelude::*;
+use util::ResultExt as _;
+use util::paths::PathStyle;
use util::rel_path::RelPath;
use workspace::Workspace;
use crate::AgentPanel;
-use crate::acp::message_editor::MessageEditor;
-use crate::context_picker::file_context_picker::{FileMatch, search_files};
-use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
-use crate::context_picker::symbol_context_picker::SymbolMatch;
-use crate::context_picker::symbol_context_picker::search_symbols;
-use crate::context_picker::thread_context_picker::search_threads;
-use crate::context_picker::{
- ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges,
-};
+use crate::mention_set::MentionSet;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum PromptContextEntry {
+ Mode(PromptContextType),
+ Action(PromptContextAction),
+}
+
+impl PromptContextEntry {
+ pub fn keyword(&self) -> &'static str {
+ match self {
+ Self::Mode(mode) => mode.keyword(),
+ Self::Action(action) => action.keyword(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum PromptContextType {
+ File,
+ Symbol,
+ Fetch,
+ Thread,
+ Rules,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum PromptContextAction {
+ AddSelections,
+}
+
+impl PromptContextAction {
+ pub fn keyword(&self) -> &'static str {
+ match self {
+ Self::AddSelections => "selection",
+ }
+ }
+
+ pub fn label(&self) -> &'static str {
+ match self {
+ Self::AddSelections => "Selection",
+ }
+ }
+
+ pub fn icon(&self) -> IconName {
+ match self {
+ Self::AddSelections => IconName::Reader,
+ }
+ }
+}
+
+impl TryFrom<&str> for PromptContextType {
+ type Error = String;
+
+ fn try_from(value: &str) -> Result<Self, Self::Error> {
+ match value {
+ "file" => Ok(Self::File),
+ "symbol" => Ok(Self::Symbol),
+ "fetch" => Ok(Self::Fetch),
+ "thread" => Ok(Self::Thread),
+ "rule" => Ok(Self::Rules),
+ _ => Err(format!("Invalid context picker mode: {}", value)),
+ }
+ }
+}
+
+impl PromptContextType {
+ pub fn keyword(&self) -> &'static str {
+ match self {
+ Self::File => "file",
+ Self::Symbol => "symbol",
+ Self::Fetch => "fetch",
+ Self::Thread => "thread",
+ Self::Rules => "rule",
+ }
+ }
+
+ pub fn label(&self) -> &'static str {
+ match self {
+ Self::File => "Files & Directories",
+ Self::Symbol => "Symbols",
+ Self::Fetch => "Fetch",
+ Self::Thread => "Threads",
+ Self::Rules => "Rules",
+ }
+ }
+
+ pub fn icon(&self) -> IconName {
+ match self {
+ Self::File => IconName::File,
+ Self::Symbol => IconName::Code,
+ Self::Fetch => IconName::ToolWeb,
+ Self::Thread => IconName::Thread,
+ Self::Rules => IconName::Reader,
+ }
+ }
+}
pub(crate) enum Match {
File(FileMatch),
@@ -47,11 +136,6 @@ pub(crate) enum Match {
Entry(EntryMatch),
}
-pub struct EntryMatch {
- mat: Option<StringMatch>,
- entry: ContextPickerEntry,
-}
-
impl Match {
pub fn score(&self) -> f64 {
match self {
@@ -66,43 +150,69 @@ impl Match {
}
}
-pub struct ContextPickerCompletionProvider {
- message_editor: WeakEntity<MessageEditor>,
- workspace: WeakEntity<Workspace>,
+pub struct EntryMatch {
+ mat: Option<StringMatch>,
+ entry: PromptContextEntry,
+}
+
+#[derive(Debug, Clone)]
+pub struct RulesContextEntry {
+ pub prompt_id: UserPromptId,
+ pub title: SharedString,
+}
+
+#[derive(Debug, Clone)]
+pub struct AvailableCommand {
+ pub name: Arc<str>,
+ pub description: Arc<str>,
+ pub requires_argument: bool,
+}
+
+pub trait PromptCompletionProviderDelegate: Send + Sync + 'static {
+ fn supports_context(&self, mode: PromptContextType, cx: &App) -> bool {
+ self.supported_modes(cx).contains(&mode)
+ }
+ fn supported_modes(&self, cx: &App) -> Vec<PromptContextType>;
+ fn supports_images(&self, cx: &App) -> bool;
+
+ fn available_commands(&self, cx: &App) -> Vec<AvailableCommand>;
+ fn confirm_command(&self, cx: &mut App);
+}
+
+pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> {
+ source: Arc<T>,
+ mention_set: Entity<MentionSet>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
- prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
- available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+ workspace: WeakEntity<Workspace>,
}
-impl ContextPickerCompletionProvider {
+impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
pub fn new(
- message_editor: WeakEntity<MessageEditor>,
- workspace: WeakEntity<Workspace>,
+ source: T,
+ mention_set: Entity<MentionSet>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
- prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
- available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+ workspace: WeakEntity<Workspace>,
) -> Self {
Self {
- message_editor,
+ source: Arc::new(source),
+ mention_set,
workspace,
history_store,
prompt_store,
- prompt_capabilities,
- available_commands,
}
}
fn completion_for_entry(
- entry: ContextPickerEntry,
+ entry: PromptContextEntry,
source_range: Range<Anchor>,
- message_editor: WeakEntity<MessageEditor>,
+ mention_set: WeakEntity<MentionSet>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
match entry {
- ContextPickerEntry::Mode(mode) => Some(Completion {
+ PromptContextEntry::Mode(mode) => Some(Completion {
replace_range: source_range,
new_text: format!("@{} ", mode.keyword()),
label: CodeLabel::plain(mode.label().to_string(), None),
@@ -117,8 +227,8 @@ impl ContextPickerCompletionProvider {
// inserted
confirm: Some(Arc::new(|_, _, _| true)),
}),
- ContextPickerEntry::Action(action) => {
- Self::completion_for_action(action, source_range, message_editor, workspace, cx)
+ PromptContextEntry::Action(action) => {
+ Self::completion_for_action(action, source_range, mention_set, workspace, cx)
}
}
}
@@ -127,7 +237,9 @@ impl ContextPickerCompletionProvider {
thread_entry: HistoryEntry,
source_range: Range<Anchor>,
recent: bool,
- editor: WeakEntity<MessageEditor>,
+ source: Arc<T>,
+ mention_set: WeakEntity<MentionSet>,
+ workspace: Entity<Workspace>,
cx: &mut App,
) -> Completion {
let uri = thread_entry.mention_uri();
@@ -155,8 +267,10 @@ impl ContextPickerCompletionProvider {
thread_entry.title().clone(),
source_range.start,
new_text_len - 1,
- editor,
uri,
+ source,
+ mention_set,
+ workspace,
)),
}
}
@@ -164,7 +278,9 @@ impl ContextPickerCompletionProvider {
fn completion_for_rules(
rule: RulesContextEntry,
source_range: Range<Anchor>,
- editor: WeakEntity<MessageEditor>,
+ source: Arc<T>,
+ mention_set: WeakEntity<MentionSet>,
+ workspace: Entity<Workspace>,
cx: &mut App,
) -> Completion {
let uri = MentionUri::Rule {
@@ -188,8 +304,10 @@ impl ContextPickerCompletionProvider {
rule.title,
source_range.start,
new_text_len - 1,
- editor,
uri,
+ source,
+ mention_set,
+ workspace,
)),
}
}
@@ -200,20 +318,18 @@ impl ContextPickerCompletionProvider {
is_recent: bool,
is_directory: bool,
source_range: Range<Anchor>,
- message_editor: WeakEntity<MessageEditor>,
+ source: Arc<T>,
+ mention_set: WeakEntity<MentionSet>,
+ workspace: Entity<Workspace>,
project: Entity<Project>,
cx: &mut App,
) -> Option<Completion> {
let path_style = project.read(cx).path_style(cx);
let (file_name, directory) =
- crate::context_picker::file_context_picker::extract_file_name_and_directory(
- &project_path.path,
- path_prefix,
- path_style,
- );
+ extract_file_name_and_directory(&project_path.path, path_prefix, path_style);
let label =
- build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
+ build_code_label_for_path(&file_name, directory.as_ref().map(|s| s.as_ref()), None, cx);
let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
@@ -246,8 +362,10 @@ impl ContextPickerCompletionProvider {
file_name,
source_range.start,
new_text_len - 1,
- message_editor,
uri,
+ source,
+ mention_set,
+ workspace,
)),
})
}
@@ -255,7 +373,8 @@ impl ContextPickerCompletionProvider {
fn completion_for_symbol(
symbol: Symbol,
source_range: Range<Anchor>,
- message_editor: WeakEntity<MessageEditor>,
+ source: Arc<T>,
+ mention_set: WeakEntity<MentionSet>,
workspace: Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
@@ -275,7 +394,12 @@ impl ContextPickerCompletionProvider {
),
};
- let label = build_symbol_label(&symbol.name, &file_name, symbol.range.start.0.row + 1, cx);
+ let label = build_code_label_for_path(
+ &symbol.name,
+ Some(&file_name),
+ Some(symbol.range.start.0.row + 1),
+ cx,
+ );
let uri = MentionUri::Symbol {
abs_path,
@@ -299,8 +423,10 @@ impl ContextPickerCompletionProvider {
symbol.name.into(),
source_range.start,
new_text_len - 1,
- message_editor,
uri,
+ source,
+ mention_set,
+ workspace,
)),
})
}
@@ -308,7 +434,9 @@ impl ContextPickerCompletionProvider {
fn completion_for_fetch(
source_range: Range<Anchor>,
url_to_fetch: SharedString,
- message_editor: WeakEntity<MessageEditor>,
+ source: Arc<T>,
+ mention_set: WeakEntity<MentionSet>,
+ workspace: Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
let new_text = format!("@fetch {} ", url_to_fetch);
@@ -333,21 +461,23 @@ impl ContextPickerCompletionProvider {
url_to_fetch.to_string().into(),
source_range.start,
new_text.len() - 1,
- message_editor,
mention_uri,
+ source,
+ mention_set,
+ workspace,
)),
})
}
pub(crate) fn completion_for_action(
- action: ContextPickerAction,
+ action: PromptContextAction,
source_range: Range<Anchor>,
- message_editor: WeakEntity<MessageEditor>,
+ mention_set: WeakEntity<MentionSet>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
let (new_text, on_action) = match action {
- ContextPickerAction::AddSelections => {
+ PromptContextAction::AddSelections => {
const PLACEHOLDER: &str = "selection ";
let selections = selection_ranges(workspace, cx)
.into_iter()
@@ -367,12 +497,12 @@ impl ContextPickerCompletionProvider {
let source_range = source_range.clone();
move |_, window: &mut Window, cx: &mut App| {
let selections = selections.clone();
- let message_editor = message_editor.clone();
+ let mention_set = mention_set.clone();
let source_range = source_range.clone();
window.defer(cx, move |window, cx| {
- message_editor
- .update(cx, |message_editor, cx| {
- message_editor.confirm_mention_for_selection(
+ mention_set
+ .update(cx, |store, cx| {
+ store.confirm_mention_for_selection(
source_range,
selections,
window,
@@ -406,12 +536,8 @@ impl ContextPickerCompletionProvider {
})
}
- fn search_slash_commands(
- &self,
- query: String,
- cx: &mut App,
- ) -> Task<Vec<acp::AvailableCommand>> {
- let commands = self.available_commands.borrow().clone();
+ fn search_slash_commands(&self, query: String, cx: &mut App) -> Task<Vec<AvailableCommand>> {
+ let commands = self.source.available_commands(cx);
if commands.is_empty() {
return Task::ready(Vec::new());
}
@@ -443,7 +569,7 @@ impl ContextPickerCompletionProvider {
fn search_mentions(
&self,
- mode: Option<ContextPickerMode>,
+ mode: Option<PromptContextType>,
query: String,
cancellation_flag: Arc<AtomicBool>,
cx: &mut App,
@@ -452,7 +578,7 @@ impl ContextPickerCompletionProvider {
return Task::ready(Vec::default());
};
match mode {
- Some(ContextPickerMode::File) => {
+ Some(PromptContextType::File) => {
let search_files_task = search_files(query, cancellation_flag, &workspace, cx);
cx.background_spawn(async move {
search_files_task
@@ -463,7 +589,7 @@ impl ContextPickerCompletionProvider {
})
}
- Some(ContextPickerMode::Symbol) => {
+ Some(PromptContextType::Symbol) => {
let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx);
cx.background_spawn(async move {
search_symbols_task
@@ -474,7 +600,7 @@ impl ContextPickerCompletionProvider {
})
}
- Some(ContextPickerMode::Thread) => {
+ Some(PromptContextType::Thread) => {
let search_threads_task =
search_threads(query, cancellation_flag, &self.history_store, cx);
cx.background_spawn(async move {
@@ -486,7 +612,7 @@ impl ContextPickerCompletionProvider {
})
}
- Some(ContextPickerMode::Fetch) => {
+ Some(PromptContextType::Fetch) => {
if !query.is_empty() {
Task::ready(vec![Match::Fetch(query.into())])
} else {
@@ -494,7 +620,7 @@ impl ContextPickerCompletionProvider {
}
}
- Some(ContextPickerMode::Rules) => {
+ Some(PromptContextType::Rules) => {
if let Some(prompt_store) = self.prompt_store.as_ref() {
let search_rules_task =
search_rules(query, cancellation_flag, prompt_store, cx);
@@ -584,9 +710,8 @@ impl ContextPickerCompletionProvider {
let mut recent = Vec::with_capacity(6);
let mut mentions = self
- .message_editor
- .read_with(cx, |message_editor, _cx| message_editor.mentions())
- .unwrap_or_default();
+ .mention_set
+ .read_with(cx, |store, _cx| store.mentions());
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let include_root_name = workspace.visible_worktrees(cx).count() > 1;
@@ -637,7 +762,7 @@ impl ContextPickerCompletionProvider {
}),
);
- if self.prompt_capabilities.borrow().embedded_context {
+ if self.source.supports_context(PromptContextType::Thread, cx) {
const RECENT_COUNT: usize = 2;
let threads = self
.history_store
@@ -658,15 +783,14 @@ impl ContextPickerCompletionProvider {
&self,
workspace: &Entity<Workspace>,
cx: &mut App,
- ) -> Vec<ContextPickerEntry> {
- let embedded_context = self.prompt_capabilities.borrow().embedded_context;
+ ) -> Vec<PromptContextEntry> {
let mut entries = vec![
- ContextPickerEntry::Mode(ContextPickerMode::File),
- ContextPickerEntry::Mode(ContextPickerMode::Symbol),
+ PromptContextEntry::Mode(PromptContextType::File),
+ PromptContextEntry::Mode(PromptContextType::Symbol),
];
- if embedded_context {
- entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
+ if self.source.supports_context(PromptContextType::Thread, cx) {
+ entries.push(PromptContextEntry::Mode(PromptContextType::Thread));
}
let has_selection = workspace
@@ -679,53 +803,25 @@ impl ContextPickerCompletionProvider {
})
});
if has_selection {
- entries.push(ContextPickerEntry::Action(
- ContextPickerAction::AddSelections,
+ entries.push(PromptContextEntry::Action(
+ PromptContextAction::AddSelections,
));
}
- if embedded_context {
- if self.prompt_store.is_some() {
- entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
- }
+ if self.prompt_store.is_some() && self.source.supports_context(PromptContextType::Rules, cx)
+ {
+ entries.push(PromptContextEntry::Mode(PromptContextType::Rules));
+ }
- entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
+ if self.source.supports_context(PromptContextType::Fetch, cx) {
+ entries.push(PromptContextEntry::Mode(PromptContextType::Fetch));
}
entries
}
}
-fn build_symbol_label(symbol_name: &str, file_name: &str, line: u32, cx: &App) -> CodeLabel {
- let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
- let mut label = CodeLabelBuilder::default();
-
- label.push_str(symbol_name, None);
- label.push_str(" ", None);
- label.push_str(&format!("{} L{}", file_name, line), comment_id);
-
- label.build()
-}
-
-fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
- let path = cx
- .theme()
- .syntax()
- .highlight_id("variable")
- .map(HighlightId);
- let mut label = CodeLabelBuilder::default();
-
- label.push_str(file_name, None);
- label.push_str(" ", None);
-
- if let Some(directory) = directory {
- label.push_str(directory, path);
- }
-
- label.build()
-}
-
-impl CompletionProvider for ContextPickerCompletionProvider {
+impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletionProvider<T> {
fn completions(
&self,
_excerpt_id: ExcerptId,
@@ -735,17 +831,13 @@ impl CompletionProvider for ContextPickerCompletionProvider {
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
- let state = buffer.update(cx, |buffer, _cx| {
+ let state = buffer.update(cx, |buffer, cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next()?;
- ContextCompletion::try_parse(
- line,
- offset_to_line,
- self.prompt_capabilities.borrow().embedded_context,
- )
+ ContextCompletion::try_parse(line, offset_to_line, &self.source.supported_modes(cx))
});
let Some(state) = state else {
return Task::ready(Ok(Vec::new()));
@@ -760,8 +852,8 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let source_range = snapshot.anchor_before(state.source_range().start)
..snapshot.anchor_after(state.source_range().end);
- let editor = self.message_editor.clone();
-
+ let source = self.source.clone();
+ let mention_set = self.mention_set.downgrade();
match state {
ContextCompletion::SlashCommand(SlashCommandCompletion {
command, argument, ..
@@ -778,7 +870,8 @@ impl CompletionProvider for ContextPickerCompletionProvider {
format!("/{} ", command.name)
};
- let is_missing_argument = argument.is_none() && command.input.is_some();
+ let is_missing_argument =
+ command.requires_argument && argument.is_none();
Completion {
replace_range: source_range.clone(),
new_text,
@@ -792,26 +885,18 @@ impl CompletionProvider for ContextPickerCompletionProvider {
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(Arc::new({
- let editor = editor.clone();
+ let source = source.clone();
move |intent, _window, cx| {
if !is_missing_argument {
cx.defer({
- let editor = editor.clone();
- move |cx| {
- editor
- .update(cx, |editor, cx| {
- match intent {
- CompletionIntent::Complete
- | CompletionIntent::CompleteWithInsert
- | CompletionIntent::CompleteWithReplace => {
- if !is_missing_argument {
- editor.send(cx);
- }
- }
- CompletionIntent::Compose => {}
- }
- })
- .ok();
+ let source = source.clone();
+ move |cx| match intent {
+ CompletionIntent::Complete
+ | CompletionIntent::CompleteWithInsert
+ | CompletionIntent::CompleteWithReplace => {
+ source.confirm_command(cx);
+ }
+ CompletionIntent::Compose => {}
}
});
}
@@ -869,7 +954,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
is_recent,
mat.is_dir,
source_range.clone(),
- editor.clone(),
+ source.clone(),
+ mention_set.clone(),
+ workspace.clone(),
project.clone(),
cx,
)
@@ -879,7 +966,8 @@ impl CompletionProvider for ContextPickerCompletionProvider {
Self::completion_for_symbol(
symbol,
source_range.clone(),
- editor.clone(),
+ source.clone(),
+ mention_set.clone(),
workspace.clone(),
cx,
)
@@ -889,7 +977,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
thread,
source_range.clone(),
false,
- editor.clone(),
+ source.clone(),
+ mention_set.clone(),
+ workspace.clone(),
cx,
)),
@@ -897,21 +987,27 @@ impl CompletionProvider for ContextPickerCompletionProvider {
thread,
source_range.clone(),
true,
- editor.clone(),
+ source.clone(),
+ mention_set.clone(),
+ workspace.clone(),
cx,
)),
Match::Rules(user_rules) => Some(Self::completion_for_rules(
user_rules,
source_range.clone(),
- editor.clone(),
+ source.clone(),
+ mention_set.clone(),
+ workspace.clone(),
cx,
)),
Match::Fetch(url) => Self::completion_for_fetch(
source_range.clone(),
url,
- editor.clone(),
+ source.clone(),
+ mention_set.clone(),
+ workspace.clone(),
cx,
),
@@ -919,7 +1015,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
Self::completion_for_entry(
entry,
source_range.clone(),
- editor.clone(),
+ mention_set.clone(),
&workspace,
cx,
)
@@ -957,27 +1053,24 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
- ContextCompletion::try_parse(
- line,
- offset_to_line,
- self.prompt_capabilities.borrow().embedded_context,
- )
- .filter(|completion| {
- // Right now we don't support completing arguments of slash commands
- let is_slash_command_with_argument = matches!(
- completion,
- ContextCompletion::SlashCommand(SlashCommandCompletion {
- argument: Some(_),
- ..
- })
- );
- !is_slash_command_with_argument
- })
- .map(|completion| {
- completion.source_range().start <= offset_to_line + position.column as usize
- && completion.source_range().end >= offset_to_line + position.column as usize
- })
- .unwrap_or(false)
+ ContextCompletion::try_parse(line, offset_to_line, &self.source.supported_modes(cx))
+ .filter(|completion| {
+ // Right now we don't support completing arguments of slash commands
+ let is_slash_command_with_argument = matches!(
+ completion,
+ ContextCompletion::SlashCommand(SlashCommandCompletion {
+ argument: Some(_),
+ ..
+ })
+ );
+ !is_slash_command_with_argument
+ })
+ .map(|completion| {
+ completion.source_range().start <= offset_to_line + position.column as usize
+ && completion.source_range().end
+ >= offset_to_line + position.column as usize
+ })
+ .unwrap_or(false)
} else {
false
}
@@ -992,27 +1085,33 @@ impl CompletionProvider for ContextPickerCompletionProvider {
}
}
-fn confirm_completion_callback(
+fn confirm_completion_callback<T: PromptCompletionProviderDelegate>(
crease_text: SharedString,
start: Anchor,
content_len: usize,
- message_editor: WeakEntity<MessageEditor>,
mention_uri: MentionUri,
+ source: Arc<T>,
+ mention_set: WeakEntity<MentionSet>,
+ workspace: Entity<Workspace>,
) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
Arc::new(move |_, window, cx| {
- let message_editor = message_editor.clone();
+ let source = source.clone();
+ let mention_set = mention_set.clone();
let crease_text = crease_text.clone();
let mention_uri = mention_uri.clone();
+ let workspace = workspace.clone();
window.defer(cx, move |window, cx| {
- message_editor
+ mention_set
.clone()
- .update(cx, |message_editor, cx| {
- message_editor
+ .update(cx, |mention_set, cx| {
+ mention_set
.confirm_mention_completion(
crease_text,
start,
content_len,
mention_uri,
+ source.supports_images(cx),
+ &workspace,
window,
cx,
)
@@ -1037,11 +1136,15 @@ impl ContextCompletion {
}
}
- fn try_parse(line: &str, offset_to_line: usize, allow_non_file_mentions: bool) -> Option<Self> {
+ fn try_parse(
+ line: &str,
+ offset_to_line: usize,
+ supported_modes: &[PromptContextType],
+ ) -> Option<Self> {
if let Some(command) = SlashCommandCompletion::try_parse(line, offset_to_line) {
Some(Self::SlashCommand(command))
} else if let Some(mention) =
- MentionCompletion::try_parse(allow_non_file_mentions, line, offset_to_line)
+ MentionCompletion::try_parse(line, offset_to_line, supported_modes)
{
Some(Self::Mention(mention))
} else {
@@ -1098,12 +1201,16 @@ impl SlashCommandCompletion {
#[derive(Debug, Default, PartialEq)]
struct MentionCompletion {
source_range: Range<usize>,
- mode: Option<ContextPickerMode>,
+ mode: Option<PromptContextType>,
argument: Option<String>,
}
impl MentionCompletion {
- fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option<Self> {
+ fn try_parse(
+ line: &str,
+ offset_to_line: usize,
+ supported_modes: &[PromptContextType],
+ ) -> Option<Self> {
let last_mention_start = line.rfind('@')?;
// No whitespace immediately after '@'
@@ -1137,8 +1244,8 @@ impl MentionCompletion {
// Safe since we check no leading whitespace above
end += mode_text.len();
- if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok()
- && (allow_non_file_mentions || matches!(parsed_mode, ContextPickerMode::File))
+ if let Some(parsed_mode) = PromptContextType::try_from(mode_text).ok()
+ && supported_modes.contains(&parsed_mode)
{
mode = Some(parsed_mode);
} else {
@@ -1172,6 +1279,339 @@ impl MentionCompletion {
}
}
+pub(crate) fn search_files(
+ query: String,
+ cancellation_flag: Arc<AtomicBool>,
+ workspace: &Entity<Workspace>,
+ cx: &App,
+) -> Task<Vec<FileMatch>> {
+ if query.is_empty() {
+ let workspace = workspace.read(cx);
+ let project = workspace.project().read(cx);
+ let visible_worktrees = workspace.visible_worktrees(cx).collect::<Vec<_>>();
+ let include_root_name = visible_worktrees.len() > 1;
+
+ let recent_matches = workspace
+ .recent_navigation_history(Some(10), cx)
+ .into_iter()
+ .map(|(project_path, _)| {
+ let path_prefix = if include_root_name {
+ project
+ .worktree_for_id(project_path.worktree_id, cx)
+ .map(|wt| wt.read(cx).root_name().into())
+ .unwrap_or_else(|| RelPath::empty().into())
+ } else {
+ RelPath::empty().into()
+ };
+
+ FileMatch {
+ mat: PathMatch {
+ score: 0.,
+ positions: Vec::new(),
+ worktree_id: project_path.worktree_id.to_usize(),
+ path: project_path.path,
+ path_prefix,
+ distance_to_relative_ancestor: 0,
+ is_dir: false,
+ },
+ is_recent: true,
+ }
+ });
+
+ let file_matches = visible_worktrees.into_iter().flat_map(|worktree| {
+ let worktree = worktree.read(cx);
+ let path_prefix: Arc<RelPath> = if include_root_name {
+ worktree.root_name().into()
+ } else {
+ RelPath::empty().into()
+ };
+ worktree.entries(false, 0).map(move |entry| FileMatch {
+ mat: PathMatch {
+ score: 0.,
+ positions: Vec::new(),
+ worktree_id: worktree.id().to_usize(),
+ path: entry.path.clone(),
+ path_prefix: path_prefix.clone(),
+ distance_to_relative_ancestor: 0,
+ is_dir: entry.is_dir(),
+ },
+ is_recent: false,
+ })
+ });
+
+ Task::ready(recent_matches.chain(file_matches).collect())
+ } else {
+ let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
+ let include_root_name = worktrees.len() > 1;
+ let candidate_sets = worktrees
+ .into_iter()
+ .map(|worktree| {
+ let worktree = worktree.read(cx);
+
+ PathMatchCandidateSet {
+ snapshot: worktree.snapshot(),
+ include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored),
+ include_root_name,
+ candidates: project::Candidates::Entries,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let executor = cx.background_executor().clone();
+ cx.foreground_executor().spawn(async move {
+ fuzzy::match_path_sets(
+ candidate_sets.as_slice(),
+ query.as_str(),
+ &None,
+ false,
+ 100,
+ &cancellation_flag,
+ executor,
+ )
+ .await
+ .into_iter()
+ .map(|mat| FileMatch {
+ mat,
+ is_recent: false,
+ })
+ .collect::<Vec<_>>()
+ })
+ }
+}
+
+pub(crate) fn search_symbols(
+ query: String,
+ cancellation_flag: Arc<AtomicBool>,
+ workspace: &Entity<Workspace>,
+ cx: &mut App,
+) -> Task<Vec<SymbolMatch>> {
+ let symbols_task = workspace.update(cx, |workspace, cx| {
+ workspace
+ .project()
+ .update(cx, |project, cx| project.symbols(&query, cx))
+ });
+ let project = workspace.read(cx).project().clone();
+ cx.spawn(async move |cx| {
+ let Some(symbols) = symbols_task.await.log_err() else {
+ return Vec::new();
+ };
+ let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
+ project
+ .update(cx, |project, cx| {
+ symbols
+ .iter()
+ .enumerate()
+ .map(|(id, symbol)| {
+ StringMatchCandidate::new(id, symbol.label.filter_text())
+ })
+ .partition(|candidate| match &symbols[candidate.id].path {
+ SymbolLocation::InProject(project_path) => project
+ .entry_for_path(project_path, cx)
+ .is_some_and(|e| !e.is_ignored),
+ SymbolLocation::OutsideProject { .. } => false,
+ })
+ })
+ .log_err()
+ else {
+ return Vec::new();
+ };
+
+ const MAX_MATCHES: usize = 100;
+ let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
+ &visible_match_candidates,
+ &query,
+ false,
+ true,
+ MAX_MATCHES,
+ &cancellation_flag,
+ cx.background_executor().clone(),
+ ));
+ let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
+ &external_match_candidates,
+ &query,
+ false,
+ true,
+ MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
+ &cancellation_flag,
+ cx.background_executor().clone(),
+ ));
+ let sort_key_for_match = |mat: &StringMatch| {
+ let symbol = &symbols[mat.candidate_id];
+ (Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
+ };
+
+ visible_matches.sort_unstable_by_key(sort_key_for_match);
+ external_matches.sort_unstable_by_key(sort_key_for_match);
+ let mut matches = visible_matches;
+ matches.append(&mut external_matches);
+
+ matches
+ .into_iter()
+ .map(|mut mat| {
+ let symbol = symbols[mat.candidate_id].clone();
+ let filter_start = symbol.label.filter_range.start;
+ for position in &mut mat.positions {
+ *position += filter_start;
+ }
+ SymbolMatch { symbol }
+ })
+ .collect()
+ })
+}
+
+pub(crate) fn search_threads(
+ query: String,
+ cancellation_flag: Arc<AtomicBool>,
+ thread_store: &Entity<HistoryStore>,
+ cx: &mut App,
+) -> Task<Vec<HistoryEntry>> {
+ let threads = thread_store.read(cx).entries().collect();
+ if query.is_empty() {
+ return Task::ready(threads);
+ }
+
+ let executor = cx.background_executor().clone();
+ cx.background_spawn(async move {
+ let candidates = threads
+ .iter()
+ .enumerate()
+ .map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
+ .collect::<Vec<_>>();
+ let matches = fuzzy::match_strings(
+ &candidates,
+ &query,
+ false,
+ true,
+ 100,
+ &cancellation_flag,
+ executor,
+ )
+ .await;
+
+ matches
+ .into_iter()
+ .map(|mat| threads[mat.candidate_id].clone())
+ .collect()
+ })
+}
+
+pub(crate) fn search_rules(
+ query: String,
+ cancellation_flag: Arc<AtomicBool>,
+ prompt_store: &Entity<PromptStore>,
+ cx: &mut App,
+) -> Task<Vec<RulesContextEntry>> {
+ let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
+ cx.background_spawn(async move {
+ search_task
+ .await
+ .into_iter()
+ .flat_map(|metadata| {
+ // Default prompts are filtered out as they are automatically included.
+ if metadata.default {
+ None
+ } else {
+ match metadata.id {
+ PromptId::EditWorkflow => None,
+ PromptId::User { uuid } => Some(RulesContextEntry {
+ prompt_id: uuid,
+ title: metadata.title?,
+ }),
+ }
+ }
+ })
+ .collect::<Vec<_>>()
+ })
+}
+
+pub struct SymbolMatch {
+ pub symbol: Symbol,
+}
+
+pub struct FileMatch {
+ pub mat: PathMatch,
+ pub is_recent: bool,
+}
+
+pub fn extract_file_name_and_directory(
+ path: &RelPath,
+ path_prefix: &RelPath,
+ path_style: PathStyle,
+) -> (SharedString, Option<SharedString>) {
+ // If path is empty, this means we're matching with the root directory itself
+ // so we use the path_prefix as the name
+ if path.is_empty() && !path_prefix.is_empty() {
+ return (path_prefix.display(path_style).to_string().into(), None);
+ }
+
+ let full_path = path_prefix.join(path);
+ let file_name = full_path.file_name().unwrap_or_default();
+ let display_path = full_path.display(path_style);
+ let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
+ (
+ file_name.to_string().into(),
+ Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
+ )
+}
+
+fn build_code_label_for_path(
+ file: &str,
+ directory: Option<&str>,
+ line_number: Option<u32>,
+ cx: &App,
+) -> CodeLabel {
+ let variable_highlight_id = cx
+ .theme()
+ .syntax()
+ .highlight_id("variable")
+ .map(HighlightId);
+ let mut label = CodeLabelBuilder::default();
+
+ label.push_str(file, None);
+ label.push_str(" ", None);
+
+ if let Some(directory) = directory {
+ label.push_str(directory, variable_highlight_id);
+ }
+ if let Some(line_number) = line_number {
+ label.push_str(&format!(" L{}", line_number), variable_highlight_id);
+ }
+ label.build()
+}
+
+fn selection_ranges(
+ workspace: &Entity<Workspace>,
+ cx: &mut App,
+) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
+ let Some(editor) = workspace
+ .read(cx)
+ .active_item(cx)
+ .and_then(|item| item.act_as::<Editor>(cx))
+ else {
+ return Vec::new();
+ };
+
+ editor.update(cx, |editor, cx| {
+ let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
+
+ let buffer = editor.buffer().clone().read(cx);
+ let snapshot = buffer.snapshot(cx);
+
+ selections
+ .into_iter()
+ .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
+ .flat_map(|range| {
+ let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
+ let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
+ if start_buffer != end_buffer {
+ return None;
+ }
+ Some((start_buffer, start..end))
+ })
+ .collect::<Vec<_>>()
+ })
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -1,764 +1,10 @@
-use agent::outline;
-use assistant_text_thread::TextThread;
-use futures::future;
-use futures::{FutureExt, future::Shared};
-use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task};
-use language::Buffer;
+use crate::mention_set::Mention;
+use gpui::{AppContext as _, Entity, Task};
use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
-use project::{Project, ProjectEntryId, ProjectPath, Worktree};
-use prompt_store::{PromptStore, UserPromptId};
-use ref_cast::RefCast;
-use rope::Point;
-use std::fmt::{self, Display, Formatter, Write as _};
-use std::hash::{Hash, Hasher};
-use std::path::PathBuf;
-use std::{ops::Range, path::Path, sync::Arc};
-use text::{Anchor, OffsetRangeExt as _};
-use ui::IconName;
-use util::markdown::MarkdownCodeBlock;
-use util::rel_path::RelPath;
-use util::{ResultExt as _, post_inc};
+use ui::App;
+use util::ResultExt as _;
-pub const RULES_ICON: IconName = IconName::Reader;
-
-pub enum ContextKind {
- File,
- Directory,
- Symbol,
- Selection,
- FetchedUrl,
- Thread,
- TextThread,
- Rules,
- Image,
-}
-
-impl ContextKind {
- pub fn icon(&self) -> IconName {
- match self {
- ContextKind::File => IconName::File,
- ContextKind::Directory => IconName::Folder,
- ContextKind::Symbol => IconName::Code,
- ContextKind::Selection => IconName::Reader,
- ContextKind::FetchedUrl => IconName::ToolWeb,
- ContextKind::Thread => IconName::Thread,
- ContextKind::TextThread => IconName::TextThread,
- ContextKind::Rules => RULES_ICON,
- ContextKind::Image => IconName::Image,
- }
- }
-}
-
-/// Handle for context that can be attached to a user message.
-///
-/// This uses IDs that are stable enough for tracking renames and identifying when context has
-/// already been added to the thread. To use this in a set, wrap it in `AgentContextKey` to opt in
-/// to `PartialEq` and `Hash` impls that use the subset of the fields used for this stable identity.
-#[derive(Debug, Clone)]
-pub enum AgentContextHandle {
- File(FileContextHandle),
- Directory(DirectoryContextHandle),
- Symbol(SymbolContextHandle),
- Selection(SelectionContextHandle),
- FetchedUrl(FetchedUrlContext),
- Thread(ThreadContextHandle),
- TextThread(TextThreadContextHandle),
- Rules(RulesContextHandle),
- Image(ImageContext),
-}
-
-impl AgentContextHandle {
- pub fn id(&self) -> ContextId {
- match self {
- Self::File(context) => context.context_id,
- Self::Directory(context) => context.context_id,
- Self::Symbol(context) => context.context_id,
- Self::Selection(context) => context.context_id,
- Self::FetchedUrl(context) => context.context_id,
- Self::Thread(context) => context.context_id,
- Self::TextThread(context) => context.context_id,
- Self::Rules(context) => context.context_id,
- Self::Image(context) => context.context_id,
- }
- }
-
- pub fn element_id(&self, name: SharedString) -> ElementId {
- ElementId::NamedInteger(name, self.id().0)
- }
-}
-
-/// Loaded context that can be attached to a user message. This can be thought of as a
-/// snapshot of the context along with an `AgentContextHandle`.
-#[derive(Debug, Clone)]
-pub enum AgentContext {
- File(FileContext),
- Directory(DirectoryContext),
- Symbol(SymbolContext),
- Selection(SelectionContext),
- FetchedUrl(FetchedUrlContext),
- Thread(ThreadContext),
- TextThread(TextThreadContext),
- Rules(RulesContext),
- Image(ImageContext),
-}
-
-impl AgentContext {
- pub fn handle(&self) -> AgentContextHandle {
- match self {
- AgentContext::File(context) => AgentContextHandle::File(context.handle.clone()),
- AgentContext::Directory(context) => {
- AgentContextHandle::Directory(context.handle.clone())
- }
- AgentContext::Symbol(context) => AgentContextHandle::Symbol(context.handle.clone()),
- AgentContext::Selection(context) => {
- AgentContextHandle::Selection(context.handle.clone())
- }
- AgentContext::FetchedUrl(context) => AgentContextHandle::FetchedUrl(context.clone()),
- AgentContext::Thread(context) => AgentContextHandle::Thread(context.handle.clone()),
- AgentContext::TextThread(context) => {
- AgentContextHandle::TextThread(context.handle.clone())
- }
- AgentContext::Rules(context) => AgentContextHandle::Rules(context.handle.clone()),
- AgentContext::Image(context) => AgentContextHandle::Image(context.clone()),
- }
- }
-}
-
-/// ID created at time of context add, for use in ElementId. This is not the stable identity of a
-/// context, instead that's handled by the `PartialEq` and `Hash` impls of `AgentContextKey`.
-#[derive(Debug, Copy, Clone)]
-pub struct ContextId(u64);
-
-impl ContextId {
- pub fn zero() -> Self {
- ContextId(0)
- }
-
- fn for_lookup() -> Self {
- ContextId(u64::MAX)
- }
-
- pub fn post_inc(&mut self) -> Self {
- Self(post_inc(&mut self.0))
- }
-}
-
-/// File context provides the entire contents of a file.
-///
-/// This holds an `Entity<Buffer>` so that file path renames affect its display and so that it can
-/// be opened even if the file has been deleted. An alternative might be to use `ProjectEntryId`,
-/// but then when deleted there is no path info or ability to open.
-#[derive(Debug, Clone)]
-pub struct FileContextHandle {
- pub buffer: Entity<Buffer>,
- pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct FileContext {
- pub handle: FileContextHandle,
- pub full_path: String,
- pub text: SharedString,
- pub is_outline: bool,
-}
-
-impl FileContextHandle {
- pub fn eq_for_key(&self, other: &Self) -> bool {
- self.buffer == other.buffer
- }
-
- pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
- self.buffer.hash(state)
- }
-
- pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
- let file = self.buffer.read(cx).file()?;
- Some(ProjectPath {
- worktree_id: file.worktree_id(cx),
- path: file.path().clone(),
- })
- }
-
- fn load(self, cx: &App) -> Task<Option<AgentContext>> {
- let buffer_ref = self.buffer.read(cx);
- let Some(file) = buffer_ref.file() else {
- log::error!("file context missing path");
- return Task::ready(None);
- };
- let full_path = file.full_path(cx).to_string_lossy().into_owned();
- let rope = buffer_ref.as_rope().clone();
- let buffer = self.buffer.clone();
-
- cx.spawn(async move |cx| {
- let buffer_content =
- outline::get_buffer_content_or_outline(buffer.clone(), Some(&full_path), &cx)
- .await
- .unwrap_or_else(|_| outline::BufferContent {
- text: rope.to_string(),
- is_outline: false,
- });
-
- let context = AgentContext::File(FileContext {
- handle: self,
- full_path,
- text: buffer_content.text.into(),
- is_outline: buffer_content.is_outline,
- });
- Some(context)
- })
- }
-}
-
-impl Display for FileContext {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}",
- MarkdownCodeBlock {
- tag: &codeblock_tag(&self.full_path, None),
- text: &self.text,
- }
- )
- }
-}
-
-/// Directory contents provides the entire contents of text files in a directory.
-///
-/// This has a `ProjectEntryId` so that it follows renames.
-#[derive(Debug, Clone)]
-pub struct DirectoryContextHandle {
- pub entry_id: ProjectEntryId,
- pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct DirectoryContext {
- pub handle: DirectoryContextHandle,
- pub full_path: String,
- pub descendants: Vec<DirectoryContextDescendant>,
-}
-
-#[derive(Debug, Clone)]
-pub struct DirectoryContextDescendant {
- /// Path within the directory.
- pub rel_path: Arc<RelPath>,
- pub fenced_codeblock: SharedString,
-}
-
-impl DirectoryContextHandle {
- pub fn eq_for_key(&self, other: &Self) -> bool {
- self.entry_id == other.entry_id
- }
-
- pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
- self.entry_id.hash(state)
- }
-
- fn load(self, project: Entity<Project>, cx: &mut App) -> Task<Option<AgentContext>> {
- let Some(worktree) = project.read(cx).worktree_for_entry(self.entry_id, cx) else {
- return Task::ready(None);
- };
- let worktree_ref = worktree.read(cx);
- let Some(entry) = worktree_ref.entry_for_id(self.entry_id) else {
- return Task::ready(None);
- };
- if entry.is_file() {
- log::error!("DirectoryContext unexpectedly refers to a file.");
- return Task::ready(None);
- }
-
- let directory_path = entry.path.clone();
- let directory_full_path = worktree_ref
- .full_path(&directory_path)
- .to_string_lossy()
- .to_string();
-
- let file_paths = collect_files_in_path(worktree_ref, &directory_path);
- let descendants_future = future::join_all(file_paths.into_iter().map(|path| {
- let worktree_ref = worktree.read(cx);
- let worktree_id = worktree_ref.id();
- let full_path = worktree_ref.full_path(&path).to_string_lossy().into_owned();
-
- let rel_path = path
- .strip_prefix(&directory_path)
- .log_err()
- .map_or_else(|| path.clone(), |rel_path| rel_path.into());
-
- let open_task = project.update(cx, |project, cx| {
- project.buffer_store().update(cx, |buffer_store, cx| {
- let project_path = ProjectPath { worktree_id, path };
- buffer_store.open_buffer(project_path, cx)
- })
- });
-
- // TODO: report load errors instead of just logging
- let rope_task = cx.spawn(async move |cx| {
- let buffer = open_task.await.log_err()?;
- let rope = buffer
- .read_with(cx, |buffer, _cx| buffer.as_rope().clone())
- .log_err()?;
- Some((rope, buffer))
- });
-
- cx.background_spawn(async move {
- let (rope, _buffer) = rope_task.await?;
- let fenced_codeblock = MarkdownCodeBlock {
- tag: &codeblock_tag(&full_path, None),
- text: &rope.to_string(),
- }
- .to_string()
- .into();
- let descendant = DirectoryContextDescendant {
- rel_path,
- fenced_codeblock,
- };
- Some(descendant)
- })
- }));
-
- cx.background_spawn(async move {
- let descendants = descendants_future
- .await
- .into_iter()
- .flatten()
- .collect::<Vec<_>>();
- let context = AgentContext::Directory(DirectoryContext {
- handle: self,
- full_path: directory_full_path,
- descendants,
- });
- Some(context)
- })
- }
-}
-
-impl Display for DirectoryContext {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let mut is_first = true;
- for descendant in &self.descendants {
- if !is_first {
- writeln!(f)?;
- } else {
- is_first = false;
- }
- write!(f, "{}", descendant.fenced_codeblock)?;
- }
- Ok(())
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct SymbolContextHandle {
- pub buffer: Entity<Buffer>,
- pub symbol: SharedString,
- pub range: Range<Anchor>,
- /// The range that fully contains the symbol. e.g. for function symbol, this will include not
- /// only the signature, but also the body. Not used by `PartialEq` or `Hash` for
- /// `AgentContextKey`.
- pub enclosing_range: Range<Anchor>,
- pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct SymbolContext {
- pub handle: SymbolContextHandle,
- pub full_path: String,
- pub line_range: Range<Point>,
- pub text: SharedString,
-}
-
-impl SymbolContextHandle {
- pub fn eq_for_key(&self, other: &Self) -> bool {
- self.buffer == other.buffer && self.symbol == other.symbol && self.range == other.range
- }
-
- pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
- self.buffer.hash(state);
- self.symbol.hash(state);
- self.range.hash(state);
- }
-
- pub fn full_path(&self, cx: &App) -> Option<PathBuf> {
- Some(self.buffer.read(cx).file()?.full_path(cx))
- }
-
- pub fn enclosing_line_range(&self, cx: &App) -> Range<Point> {
- self.enclosing_range
- .to_point(&self.buffer.read(cx).snapshot())
- }
-
- pub fn text(&self, cx: &App) -> SharedString {
- self.buffer
- .read(cx)
- .text_for_range(self.enclosing_range.clone())
- .collect::<String>()
- .into()
- }
-
- fn load(self, cx: &App) -> Task<Option<AgentContext>> {
- let buffer_ref = self.buffer.read(cx);
- let Some(file) = buffer_ref.file() else {
- log::error!("symbol context's file has no path");
- return Task::ready(None);
- };
- let full_path = file.full_path(cx).to_string_lossy().into_owned();
- let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot());
- let text = self.text(cx);
- let context = AgentContext::Symbol(SymbolContext {
- handle: self,
- full_path,
- line_range,
- text,
- });
- Task::ready(Some(context))
- }
-}
-
-impl Display for SymbolContext {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let code_block = MarkdownCodeBlock {
- tag: &codeblock_tag(&self.full_path, Some(self.line_range.clone())),
- text: &self.text,
- };
- write!(f, "{code_block}",)
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct SelectionContextHandle {
- pub buffer: Entity<Buffer>,
- pub range: Range<Anchor>,
- pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct SelectionContext {
- pub handle: SelectionContextHandle,
- pub full_path: String,
- pub line_range: Range<Point>,
- pub text: SharedString,
-}
-
-impl SelectionContextHandle {
- pub fn eq_for_key(&self, other: &Self) -> bool {
- self.buffer == other.buffer && self.range == other.range
- }
-
- pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
- self.buffer.hash(state);
- self.range.hash(state);
- }
-
- pub fn full_path(&self, cx: &App) -> Option<PathBuf> {
- Some(self.buffer.read(cx).file()?.full_path(cx))
- }
-
- pub fn line_range(&self, cx: &App) -> Range<Point> {
- self.range.to_point(&self.buffer.read(cx).snapshot())
- }
-
- pub fn text(&self, cx: &App) -> SharedString {
- self.buffer
- .read(cx)
- .text_for_range(self.range.clone())
- .collect::<String>()
- .into()
- }
-
- fn load(self, cx: &App) -> Task<Option<AgentContext>> {
- let Some(full_path) = self.full_path(cx) else {
- log::error!("selection context's file has no path");
- return Task::ready(None);
- };
- let text = self.text(cx);
- let context = AgentContext::Selection(SelectionContext {
- full_path: full_path.to_string_lossy().into_owned(),
- line_range: self.line_range(cx),
- text,
- handle: self,
- });
-
- Task::ready(Some(context))
- }
-}
-
-impl Display for SelectionContext {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let code_block = MarkdownCodeBlock {
- tag: &codeblock_tag(&self.full_path, Some(self.line_range.clone())),
- text: &self.text,
- };
- write!(f, "{code_block}",)
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct FetchedUrlContext {
- pub url: SharedString,
- /// Text contents of the fetched url. Unlike other context types, the contents of this gets
- /// populated when added rather than when sending the message. Not used by `PartialEq` or `Hash`
- /// for `AgentContextKey`.
- pub text: SharedString,
- pub context_id: ContextId,
-}
-
-impl FetchedUrlContext {
- pub fn eq_for_key(&self, other: &Self) -> bool {
- self.url == other.url
- }
-
- pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
- self.url.hash(state);
- }
-
- pub fn lookup_key(url: SharedString) -> AgentContextKey {
- AgentContextKey(AgentContextHandle::FetchedUrl(FetchedUrlContext {
- url,
- text: "".into(),
- context_id: ContextId::for_lookup(),
- }))
- }
-
- pub fn load(self) -> Task<Option<AgentContext>> {
- Task::ready(Some(AgentContext::FetchedUrl(self)))
- }
-}
-
-impl Display for FetchedUrlContext {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- // TODO: Better format - url and contents are not delimited.
- write!(f, "{}\n{}\n", self.url, self.text)
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct ThreadContextHandle {
- pub thread: Entity<agent::Thread>,
- pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct ThreadContext {
- pub handle: ThreadContextHandle,
- pub title: SharedString,
- pub text: SharedString,
-}
-
-impl ThreadContextHandle {
- pub fn eq_for_key(&self, other: &Self) -> bool {
- self.thread == other.thread
- }
-
- pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
- self.thread.hash(state)
- }
-
- pub fn title(&self, cx: &App) -> SharedString {
- self.thread.read(cx).title()
- }
-
- fn load(self, cx: &mut App) -> Task<Option<AgentContext>> {
- let task = self.thread.update(cx, |thread, cx| thread.summary(cx));
- let title = self.title(cx);
- cx.background_spawn(async move {
- let text = task.await?;
- let context = AgentContext::Thread(ThreadContext {
- title,
- text,
- handle: self,
- });
- Some(context)
- })
- }
-}
-
-impl Display for ThreadContext {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- // TODO: Better format for this - doesn't distinguish title and contents.
- write!(f, "{}\n{}\n", &self.title, &self.text.trim())
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct TextThreadContextHandle {
- pub text_thread: Entity<TextThread>,
- pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct TextThreadContext {
- pub handle: TextThreadContextHandle,
- pub title: SharedString,
- pub text: SharedString,
-}
-
-impl TextThreadContextHandle {
- // pub fn lookup_key() ->
- pub fn eq_for_key(&self, other: &Self) -> bool {
- self.text_thread == other.text_thread
- }
-
- pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
- self.text_thread.hash(state)
- }
-
- pub fn title(&self, cx: &App) -> SharedString {
- self.text_thread.read(cx).summary().or_default()
- }
-
- fn load(self, cx: &App) -> Task<Option<AgentContext>> {
- let title = self.title(cx);
- let text = self.text_thread.read(cx).to_xml(cx);
- let context = AgentContext::TextThread(TextThreadContext {
- title,
- text: text.into(),
- handle: self,
- });
- Task::ready(Some(context))
- }
-}
-
-impl Display for TextThreadContext {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- write!(f, "<text_thread title=\"")?;
- for c in self.title.chars() {
- match c {
- '&' => write!(f, "&")?,
- '<' => write!(f, "<")?,
- '>' => write!(f, ">")?,
- '"' => write!(f, """)?,
- '\'' => write!(f, "'")?,
- _ => write!(f, "{}", c)?,
- }
- }
- writeln!(f, "\">")?;
- write!(f, "{}", self.text.trim())?;
- write!(f, "\n</text_thread>")
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct RulesContextHandle {
- pub prompt_id: UserPromptId,
- pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct RulesContext {
- pub handle: RulesContextHandle,
- pub title: Option<SharedString>,
- pub text: SharedString,
-}
-
-impl RulesContextHandle {
- pub fn eq_for_key(&self, other: &Self) -> bool {
- self.prompt_id == other.prompt_id
- }
-
- pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
- self.prompt_id.hash(state)
- }
-
- pub fn lookup_key(prompt_id: UserPromptId) -> AgentContextKey {
- AgentContextKey(AgentContextHandle::Rules(RulesContextHandle {
- prompt_id,
- context_id: ContextId::for_lookup(),
- }))
- }
-
- pub fn load(
- self,
- prompt_store: &Option<Entity<PromptStore>>,
- cx: &App,
- ) -> Task<Option<AgentContext>> {
- let Some(prompt_store) = prompt_store.as_ref() else {
- return Task::ready(None);
- };
- let prompt_store = prompt_store.read(cx);
- let prompt_id = self.prompt_id.into();
- let Some(metadata) = prompt_store.metadata(prompt_id) else {
- return Task::ready(None);
- };
- let title = metadata.title;
- let text_task = prompt_store.load(prompt_id, cx);
- cx.background_spawn(async move {
- // TODO: report load errors instead of just logging
- let text = text_task.await.log_err()?.into();
- let context = AgentContext::Rules(RulesContext {
- handle: self,
- title,
- text,
- });
- Some(context)
- })
- }
-}
-
-impl Display for RulesContext {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- if let Some(title) = &self.title {
- writeln!(f, "Rules title: {}", title)?;
- }
- let code_block = MarkdownCodeBlock {
- tag: "",
- text: self.text.trim(),
- };
- write!(f, "{code_block}")
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct ImageContext {
- pub project_path: Option<ProjectPath>,
- pub full_path: Option<String>,
- pub original_image: Arc<gpui::Image>,
- // TODO: handle this elsewhere and remove `ignore-interior-mutability` opt-out in clippy.toml
- // needed due to a false positive of `clippy::mutable_key_type`.
- pub image_task: Shared<Task<Option<LanguageModelImage>>>,
- pub context_id: ContextId,
-}
-
-pub enum ImageStatus {
- Loading,
- Error,
- Warning,
- Ready,
-}
-
-impl ImageContext {
- pub fn eq_for_key(&self, other: &Self) -> bool {
- self.original_image.id() == other.original_image.id()
- }
-
- pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
- self.original_image.id().hash(state);
- }
-
- pub fn image(&self) -> Option<LanguageModelImage> {
- self.image_task.clone().now_or_never().flatten()
- }
-
- pub fn status(&self, model: Option<&Arc<dyn language_model::LanguageModel>>) -> ImageStatus {
- match self.image_task.clone().now_or_never() {
- None => ImageStatus::Loading,
- Some(None) => ImageStatus::Error,
- Some(Some(_)) => {
- if model.is_some_and(|model| !model.supports_images()) {
- ImageStatus::Warning
- } else {
- ImageStatus::Ready
- }
- }
- }
- }
-
- pub fn load(self, cx: &App) -> Task<Option<AgentContext>> {
- cx.background_spawn(async move {
- self.image_task.clone().await;
- Some(AgentContext::Image(self))
- })
- }
-}
+use crate::mention_set::MentionSet;
#[derive(Debug, Clone, Default)]
pub struct LoadedContext {
@@ -792,382 +38,26 @@ impl LoadedContext {
}
/// Loads and formats a collection of contexts.
-pub fn load_context(
- contexts: Vec<AgentContextHandle>,
- project: &Entity<Project>,
- prompt_store: &Option<Entity<PromptStore>>,
- cx: &mut App,
-) -> Task<LoadedContext> {
- let load_tasks: Vec<_> = contexts
- .into_iter()
- .map(|context| match context {
- AgentContextHandle::File(context) => context.load(cx),
- AgentContextHandle::Directory(context) => context.load(project.clone(), cx),
- AgentContextHandle::Symbol(context) => context.load(cx),
- AgentContextHandle::Selection(context) => context.load(cx),
- AgentContextHandle::FetchedUrl(context) => context.load(),
- AgentContextHandle::Thread(context) => context.load(cx),
- AgentContextHandle::TextThread(context) => context.load(cx),
- AgentContextHandle::Rules(context) => context.load(prompt_store, cx),
- AgentContextHandle::Image(context) => context.load(cx),
- })
- .collect();
-
+pub fn load_context(mention_set: &Entity<MentionSet>, cx: &mut App) -> Task<Option<LoadedContext>> {
+ let task = mention_set.update(cx, |mention_set, cx| mention_set.contents(true, cx));
cx.background_spawn(async move {
- let load_results = future::join_all(load_tasks).await;
-
- let mut text = String::new();
-
- let mut file_context = Vec::new();
- let mut directory_context = Vec::new();
- let mut symbol_context = Vec::new();
- let mut selection_context = Vec::new();
- let mut fetched_url_context = Vec::new();
- let mut thread_context = Vec::new();
- let mut text_thread_context = Vec::new();
- let mut rules_context = Vec::new();
- let mut images = Vec::new();
- for context in load_results.into_iter().flatten() {
- match context {
- AgentContext::File(context) => file_context.push(context),
- AgentContext::Directory(context) => directory_context.push(context),
- AgentContext::Symbol(context) => symbol_context.push(context),
- AgentContext::Selection(context) => selection_context.push(context),
- AgentContext::FetchedUrl(context) => fetched_url_context.push(context),
- AgentContext::Thread(context) => thread_context.push(context),
- AgentContext::TextThread(context) => text_thread_context.push(context),
- AgentContext::Rules(context) => rules_context.push(context),
- AgentContext::Image(context) => images.extend(context.image()),
- }
- }
-
- // Use empty text if there are no contexts that contribute to text (everything but image
- // context).
- if file_context.is_empty()
- && directory_context.is_empty()
- && symbol_context.is_empty()
- && selection_context.is_empty()
- && fetched_url_context.is_empty()
- && thread_context.is_empty()
- && text_thread_context.is_empty()
- && rules_context.is_empty()
- {
- return LoadedContext { text, images };
- }
-
- text.push_str(
- "\n<context>\n\
- The following items were attached by the user. \
- They are up-to-date and don't need to be re-read.\n\n",
- );
-
- if !file_context.is_empty() {
- text.push_str("<files>");
- for context in file_context {
- text.push('\n');
- let _ = write!(text, "{context}");
- }
- text.push_str("</files>\n");
- }
-
- if !directory_context.is_empty() {
- text.push_str("<directories>");
- for context in directory_context {
- text.push('\n');
- let _ = write!(text, "{context}");
- }
- text.push_str("</directories>\n");
- }
-
- if !symbol_context.is_empty() {
- text.push_str("<symbols>");
- for context in symbol_context {
- text.push('\n');
- let _ = write!(text, "{context}");
- }
- text.push_str("</symbols>\n");
- }
-
- if !selection_context.is_empty() {
- text.push_str("<selections>");
- for context in selection_context {
- text.push('\n');
- let _ = write!(text, "{context}");
- }
- text.push_str("</selections>\n");
- }
-
- if !fetched_url_context.is_empty() {
- text.push_str("<fetched_urls>");
- for context in fetched_url_context {
- text.push('\n');
- let _ = write!(text, "{context}");
- }
- text.push_str("</fetched_urls>\n");
- }
-
- if !thread_context.is_empty() {
- text.push_str("<conversation_threads>");
- for context in thread_context {
- text.push('\n');
- let _ = write!(text, "{context}");
- }
- text.push_str("</conversation_threads>\n");
- }
-
- if !text_thread_context.is_empty() {
- text.push_str("<text_threads>");
- for context in text_thread_context {
- text.push('\n');
- let _ = writeln!(text, "{context}");
- }
- text.push_str("<text_threads>");
- }
-
- if !rules_context.is_empty() {
- text.push_str(
- "<user_rules>\n\
- The user has specified the following rules that should be applied:\n",
- );
- for context in rules_context {
- text.push('\n');
- let _ = write!(text, "{context}");
- }
- text.push_str("</user_rules>\n");
- }
-
- text.push_str("</context>\n");
-
- LoadedContext { text, images }
+ let mentions = task.await.log_err()?;
+ let mut loaded_context = LoadedContext::default();
+ loaded_context
+ .text
+ .push_str("The following items were attached by the user.\n");
+ for (_, (_, mention)) in mentions {
+ match mention {
+ Mention::Text { content, .. } => {
+ loaded_context.text.push_str(&content);
+ }
+ Mention::Image(mention_image) => loaded_context.images.push(LanguageModelImage {
+ source: mention_image.data,
+ ..LanguageModelImage::empty()
+ }),
+ Mention::Link => {}
+ }
+ }
+ Some(loaded_context)
})
}
-
-fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<Arc<RelPath>> {
- let mut files = Vec::new();
-
- for entry in worktree.child_entries(path) {
- if entry.is_dir() {
- files.extend(collect_files_in_path(worktree, &entry.path));
- } else if entry.is_file() {
- files.push(entry.path.clone());
- }
- }
-
- files
-}
-
-fn codeblock_tag(full_path: &str, line_range: Option<Range<Point>>) -> String {
- let mut result = String::new();
-
- if let Some(extension) = Path::new(full_path)
- .extension()
- .and_then(|ext| ext.to_str())
- {
- let _ = write!(result, "{} ", extension);
- }
-
- let _ = write!(result, "{}", full_path);
-
- if let Some(range) = line_range {
- if range.start.row == range.end.row {
- let _ = write!(result, ":{}", range.start.row + 1);
- } else {
- let _ = write!(result, ":{}-{}", range.start.row + 1, range.end.row + 1);
- }
- }
-
- result
-}
-
-/// Wraps `AgentContext` to opt-in to `PartialEq` and `Hash` impls which use a subset of fields
-/// needed for stable context identity.
-#[derive(Debug, Clone, RefCast)]
-#[repr(transparent)]
-pub struct AgentContextKey(pub AgentContextHandle);
-
-impl AsRef<AgentContextHandle> for AgentContextKey {
- fn as_ref(&self) -> &AgentContextHandle {
- &self.0
- }
-}
-
-impl Eq for AgentContextKey {}
-
-impl PartialEq for AgentContextKey {
- fn eq(&self, other: &Self) -> bool {
- match &self.0 {
- AgentContextHandle::File(context) => {
- if let AgentContextHandle::File(other_context) = &other.0 {
- return context.eq_for_key(other_context);
- }
- }
- AgentContextHandle::Directory(context) => {
- if let AgentContextHandle::Directory(other_context) = &other.0 {
- return context.eq_for_key(other_context);
- }
- }
- AgentContextHandle::Symbol(context) => {
- if let AgentContextHandle::Symbol(other_context) = &other.0 {
- return context.eq_for_key(other_context);
- }
- }
- AgentContextHandle::Selection(context) => {
- if let AgentContextHandle::Selection(other_context) = &other.0 {
- return context.eq_for_key(other_context);
- }
- }
- AgentContextHandle::FetchedUrl(context) => {
- if let AgentContextHandle::FetchedUrl(other_context) = &other.0 {
- return context.eq_for_key(other_context);
- }
- }
- AgentContextHandle::Thread(context) => {
- if let AgentContextHandle::Thread(other_context) = &other.0 {
- return context.eq_for_key(other_context);
- }
- }
- AgentContextHandle::Rules(context) => {
- if let AgentContextHandle::Rules(other_context) = &other.0 {
- return context.eq_for_key(other_context);
- }
- }
- AgentContextHandle::Image(context) => {
- if let AgentContextHandle::Image(other_context) = &other.0 {
- return context.eq_for_key(other_context);
- }
- }
- AgentContextHandle::TextThread(context) => {
- if let AgentContextHandle::TextThread(other_context) = &other.0 {
- return context.eq_for_key(other_context);
- }
- }
- }
- false
- }
-}
-
-impl Hash for AgentContextKey {
- fn hash<H: Hasher>(&self, state: &mut H) {
- match &self.0 {
- AgentContextHandle::File(context) => context.hash_for_key(state),
- AgentContextHandle::Directory(context) => context.hash_for_key(state),
- AgentContextHandle::Symbol(context) => context.hash_for_key(state),
- AgentContextHandle::Selection(context) => context.hash_for_key(state),
- AgentContextHandle::FetchedUrl(context) => context.hash_for_key(state),
- AgentContextHandle::Thread(context) => context.hash_for_key(state),
- AgentContextHandle::TextThread(context) => context.hash_for_key(state),
- AgentContextHandle::Rules(context) => context.hash_for_key(state),
- AgentContextHandle::Image(context) => context.hash_for_key(state),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use gpui::TestAppContext;
- use project::{FakeFs, Project};
- use serde_json::json;
- use settings::SettingsStore;
- use util::path;
-
- fn init_test_settings(cx: &mut TestAppContext) {
- cx.update(|cx| {
- let settings_store = SettingsStore::test(cx);
- cx.set_global(settings_store);
- });
- }
-
- // Helper to create a test project with test files
- async fn create_test_project(
- cx: &mut TestAppContext,
- files: serde_json::Value,
- ) -> Entity<Project> {
- let fs = FakeFs::new(cx.background_executor.clone());
- fs.insert_tree(path!("/test"), files).await;
- Project::test(fs, [path!("/test").as_ref()], cx).await
- }
-
- #[gpui::test]
- async fn test_large_file_uses_fallback(cx: &mut TestAppContext) {
- init_test_settings(cx);
-
- // Create a large file that exceeds AUTO_OUTLINE_SIZE
- const LINE: &str = "Line with some text\n";
- let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
- let content_len = large_content.len();
-
- assert!(content_len > outline::AUTO_OUTLINE_SIZE);
-
- let file_context = load_context_for("file.txt", large_content, cx).await;
-
- // Should contain some of the actual file content
- assert!(
- file_context.text.contains(LINE),
- "Should contain some of the file content"
- );
-
- // Should be much smaller than original
- assert!(
- file_context.text.len() < content_len / 10,
- "Should be significantly smaller than original content"
- );
- }
-
- #[gpui::test]
- async fn test_small_file_uses_full_content(cx: &mut TestAppContext) {
- init_test_settings(cx);
-
- let small_content = "This is a small file.\n";
- let content_len = small_content.len();
-
- assert!(content_len < outline::AUTO_OUTLINE_SIZE);
-
- let file_context = load_context_for("file.txt", small_content.to_string(), cx).await;
-
- assert!(
- !file_context
- .text
- .contains(&format!("# File outline for {}", path!("test/file.txt"))),
- "Small files should not get an outline"
- );
-
- assert!(
- file_context.text.contains(small_content),
- "Small files should use full content"
- );
- }
-
- async fn load_context_for(
- filename: &str,
- content: String,
- cx: &mut TestAppContext,
- ) -> LoadedContext {
- // Create a test project with the file
- let project = create_test_project(
- cx,
- json!({
- filename: content,
- }),
- )
- .await;
-
- // Open the buffer
- let buffer_path = project
- .read_with(cx, |project, cx| project.find_project_path(filename, cx))
- .unwrap();
-
- let buffer = project
- .update(cx, |project, cx| project.open_buffer(buffer_path, cx))
- .await
- .unwrap();
-
- let context_handle = AgentContextHandle::File(FileContextHandle {
- buffer: buffer.clone(),
- context_id: ContextId::zero(),
- });
-
- cx.update(|cx| load_context(vec![context_handle], &project, &None, cx))
- .await
- }
-}
@@ -1,931 +0,0 @@
-mod completion_provider;
-pub(crate) mod fetch_context_picker;
-pub(crate) mod file_context_picker;
-pub(crate) mod rules_context_picker;
-pub(crate) mod symbol_context_picker;
-pub(crate) mod thread_context_picker;
-
-use std::ops::Range;
-use std::path::PathBuf;
-use std::sync::Arc;
-
-use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
-use agent_client_protocol as acp;
-use anyhow::{Result, anyhow};
-use collections::HashSet;
-pub use completion_provider::ContextPickerCompletionProvider;
-use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
-use editor::{Anchor, Editor, ExcerptId, FoldPlaceholder, ToOffset};
-use fetch_context_picker::FetchContextPicker;
-use file_context_picker::FileContextPicker;
-use file_context_picker::render_file_context_entry;
-use gpui::{
- App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
- WeakEntity,
-};
-use language::Buffer;
-use multi_buffer::MultiBufferRow;
-use project::ProjectPath;
-use prompt_store::PromptStore;
-use rules_context_picker::{RulesContextEntry, RulesContextPicker};
-use symbol_context_picker::SymbolContextPicker;
-use thread_context_picker::render_thread_context_entry;
-use ui::{
- ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
-};
-use util::paths::PathStyle;
-use util::rel_path::RelPath;
-use workspace::{Workspace, notifications::NotifyResultExt};
-
-use crate::context_picker::thread_context_picker::ThreadContextPicker;
-use crate::{context::RULES_ICON, context_store::ContextStore};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum ContextPickerEntry {
- Mode(ContextPickerMode),
- Action(ContextPickerAction),
-}
-
-impl ContextPickerEntry {
- pub fn keyword(&self) -> &'static str {
- match self {
- Self::Mode(mode) => mode.keyword(),
- Self::Action(action) => action.keyword(),
- }
- }
-
- pub fn label(&self) -> &'static str {
- match self {
- Self::Mode(mode) => mode.label(),
- Self::Action(action) => action.label(),
- }
- }
-
- pub fn icon(&self) -> IconName {
- match self {
- Self::Mode(mode) => mode.icon(),
- Self::Action(action) => action.icon(),
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum ContextPickerMode {
- File,
- Symbol,
- Fetch,
- Thread,
- Rules,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum ContextPickerAction {
- AddSelections,
-}
-
-impl ContextPickerAction {
- pub fn keyword(&self) -> &'static str {
- match self {
- Self::AddSelections => "selection",
- }
- }
-
- pub fn label(&self) -> &'static str {
- match self {
- Self::AddSelections => "Selection",
- }
- }
-
- pub fn icon(&self) -> IconName {
- match self {
- Self::AddSelections => IconName::Reader,
- }
- }
-}
-
-impl TryFrom<&str> for ContextPickerMode {
- type Error = String;
-
- fn try_from(value: &str) -> Result<Self, Self::Error> {
- match value {
- "file" => Ok(Self::File),
- "symbol" => Ok(Self::Symbol),
- "fetch" => Ok(Self::Fetch),
- "thread" => Ok(Self::Thread),
- "rule" => Ok(Self::Rules),
- _ => Err(format!("Invalid context picker mode: {}", value)),
- }
- }
-}
-
-impl ContextPickerMode {
- pub fn keyword(&self) -> &'static str {
- match self {
- Self::File => "file",
- Self::Symbol => "symbol",
- Self::Fetch => "fetch",
- Self::Thread => "thread",
- Self::Rules => "rule",
- }
- }
-
- pub fn label(&self) -> &'static str {
- match self {
- Self::File => "Files & Directories",
- Self::Symbol => "Symbols",
- Self::Fetch => "Fetch",
- Self::Thread => "Threads",
- Self::Rules => "Rules",
- }
- }
-
- pub fn icon(&self) -> IconName {
- match self {
- Self::File => IconName::File,
- Self::Symbol => IconName::Code,
- Self::Fetch => IconName::ToolWeb,
- Self::Thread => IconName::Thread,
- Self::Rules => RULES_ICON,
- }
- }
-}
-
-#[derive(Debug, Clone)]
-enum ContextPickerState {
- Default(Entity<ContextMenu>),
- File(Entity<FileContextPicker>),
- Symbol(Entity<SymbolContextPicker>),
- Fetch(Entity<FetchContextPicker>),
- Thread(Entity<ThreadContextPicker>),
- Rules(Entity<RulesContextPicker>),
-}
-
-pub(super) struct ContextPicker {
- mode: ContextPickerState,
- workspace: WeakEntity<Workspace>,
- context_store: WeakEntity<ContextStore>,
- thread_store: Option<WeakEntity<HistoryStore>>,
- prompt_store: Option<WeakEntity<PromptStore>>,
- _subscriptions: Vec<Subscription>,
-}
-
-impl ContextPicker {
- pub fn new(
- workspace: WeakEntity<Workspace>,
- thread_store: Option<WeakEntity<HistoryStore>>,
- prompt_store: Option<WeakEntity<PromptStore>>,
- context_store: WeakEntity<ContextStore>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let subscriptions = context_store
- .upgrade()
- .map(|context_store| {
- cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx))
- })
- .into_iter()
- .chain(
- thread_store
- .as_ref()
- .and_then(|thread_store| thread_store.upgrade())
- .map(|thread_store| {
- cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx))
- }),
- )
- .collect::<Vec<Subscription>>();
-
- ContextPicker {
- mode: ContextPickerState::Default(ContextMenu::build(
- window,
- cx,
- |menu, _window, _cx| menu,
- )),
- workspace,
- context_store,
- thread_store,
- prompt_store,
- _subscriptions: subscriptions,
- }
- }
-
- pub fn init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.mode = ContextPickerState::Default(self.build_menu(window, cx));
- cx.notify();
- }
-
- fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {
- let context_picker = cx.entity();
-
- let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
- let Some(workspace) = self.workspace.upgrade() else {
- return menu;
- };
- let path_style = workspace.read(cx).path_style(cx);
- let recent = self.recent_entries(cx);
- let has_recent = !recent.is_empty();
- let recent_entries = recent
- .into_iter()
- .enumerate()
- .map(|(ix, entry)| {
- self.recent_menu_item(context_picker.clone(), ix, entry, path_style)
- })
- .collect::<Vec<_>>();
-
- let entries = self
- .workspace
- .upgrade()
- .map(|workspace| {
- available_context_picker_entries(
- &self.prompt_store,
- &self.thread_store,
- &workspace,
- cx,
- )
- })
- .unwrap_or_default();
-
- menu.when(has_recent, |menu| {
- menu.custom_row(|_, _| {
- div()
- .mb_1()
- .child(
- Label::new("Recent")
- .color(Color::Muted)
- .size(LabelSize::Small),
- )
- .into_any_element()
- })
- })
- .extend(recent_entries)
- .when(has_recent, |menu| menu.separator())
- .extend(entries.into_iter().map(|entry| {
- let context_picker = context_picker.clone();
-
- ContextMenuEntry::new(entry.label())
- .icon(entry.icon())
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .handler(move |window, cx| {
- context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
- })
- }))
- .keep_open_on_confirm(true)
- });
-
- cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
- cx.emit(DismissEvent);
- })
- .detach();
-
- menu
- }
-
- /// Whether threads are allowed as context.
- pub fn allow_threads(&self) -> bool {
- self.thread_store.is_some()
- }
-
- fn select_entry(
- &mut self,
- entry: ContextPickerEntry,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let context_picker = cx.entity().downgrade();
-
- match entry {
- ContextPickerEntry::Mode(mode) => match mode {
- ContextPickerMode::File => {
- self.mode = ContextPickerState::File(cx.new(|cx| {
- FileContextPicker::new(
- context_picker.clone(),
- self.workspace.clone(),
- self.context_store.clone(),
- window,
- cx,
- )
- }));
- }
- ContextPickerMode::Symbol => {
- self.mode = ContextPickerState::Symbol(cx.new(|cx| {
- SymbolContextPicker::new(
- context_picker.clone(),
- self.workspace.clone(),
- self.context_store.clone(),
- window,
- cx,
- )
- }));
- }
- ContextPickerMode::Rules => {
- if let Some(prompt_store) = self.prompt_store.as_ref() {
- self.mode = ContextPickerState::Rules(cx.new(|cx| {
- RulesContextPicker::new(
- prompt_store.clone(),
- context_picker.clone(),
- self.context_store.clone(),
- window,
- cx,
- )
- }));
- }
- }
- ContextPickerMode::Fetch => {
- self.mode = ContextPickerState::Fetch(cx.new(|cx| {
- FetchContextPicker::new(
- context_picker.clone(),
- self.workspace.clone(),
- self.context_store.clone(),
- window,
- cx,
- )
- }));
- }
- ContextPickerMode::Thread => {
- if let Some(thread_store) = self.thread_store.clone() {
- self.mode = ContextPickerState::Thread(cx.new(|cx| {
- ThreadContextPicker::new(
- thread_store,
- context_picker.clone(),
- self.context_store.clone(),
- self.workspace.clone(),
- window,
- cx,
- )
- }));
- }
- }
- },
- ContextPickerEntry::Action(action) => match action {
- ContextPickerAction::AddSelections => {
- if let Some((context_store, workspace)) =
- self.context_store.upgrade().zip(self.workspace.upgrade())
- {
- add_selections_as_context(&context_store, &workspace, cx);
- }
-
- cx.emit(DismissEvent);
- }
- },
- }
-
- cx.notify();
- cx.focus_self(window);
- }
-
- pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- // Other variants already select their first entry on open automatically
- if let ContextPickerState::Default(entity) = &self.mode {
- entity.update(cx, |entity, cx| {
- entity.select_first(&Default::default(), window, cx)
- })
- }
- }
-
- fn recent_menu_item(
- &self,
- context_picker: Entity<ContextPicker>,
- ix: usize,
- entry: RecentEntry,
- path_style: PathStyle,
- ) -> ContextMenuItem {
- match entry {
- RecentEntry::File {
- project_path,
- path_prefix,
- } => {
- let context_store = self.context_store.clone();
- let worktree_id = project_path.worktree_id;
- let path = project_path.path.clone();
-
- ContextMenuItem::custom_entry(
- move |_window, cx| {
- render_file_context_entry(
- ElementId::named_usize("ctx-recent", ix),
- worktree_id,
- &path,
- &path_prefix,
- false,
- path_style,
- context_store.clone(),
- cx,
- )
- .into_any()
- },
- move |window, cx| {
- context_picker.update(cx, |this, cx| {
- this.add_recent_file(project_path.clone(), window, cx);
- })
- },
- None,
- )
- }
- RecentEntry::Thread(thread) => {
- let context_store = self.context_store.clone();
- let view_thread = thread.clone();
-
- ContextMenuItem::custom_entry(
- move |_window, cx| {
- render_thread_context_entry(&view_thread, context_store.clone(), cx)
- .into_any()
- },
- move |window, cx| {
- context_picker.update(cx, |this, cx| {
- this.add_recent_thread(thread.clone(), window, cx)
- .detach_and_log_err(cx);
- })
- },
- None,
- )
- }
- }
- }
-
- fn add_recent_file(
- &self,
- project_path: ProjectPath,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let Some(context_store) = self.context_store.upgrade() else {
- return;
- };
-
- let task = context_store.update(cx, |context_store, cx| {
- context_store.add_file_from_path(project_path.clone(), true, cx)
- });
-
- cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
- .detach();
-
- cx.notify();
- }
-
- fn add_recent_thread(
- &self,
- entry: HistoryEntry,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Task<Result<()>> {
- let Some(context_store) = self.context_store.upgrade() else {
- return Task::ready(Err(anyhow!("context store not available")));
- };
- let Some(project) = self
- .workspace
- .upgrade()
- .map(|workspace| workspace.read(cx).project().clone())
- else {
- return Task::ready(Err(anyhow!("project not available")));
- };
-
- match entry {
- HistoryEntry::AcpThread(thread) => {
- let Some(thread_store) = self
- .thread_store
- .as_ref()
- .and_then(|thread_store| thread_store.upgrade())
- else {
- return Task::ready(Err(anyhow!("thread store not available")));
- };
- let load_thread_task =
- agent::load_agent_thread(thread.id, thread_store, project, cx);
- cx.spawn(async move |this, cx| {
- let thread = load_thread_task.await?;
- context_store.update(cx, |context_store, cx| {
- context_store.add_thread(thread, true, cx);
- })?;
- this.update(cx, |_this, cx| cx.notify())
- })
- }
- HistoryEntry::TextThread(thread) => {
- let Some(thread_store) = self
- .thread_store
- .as_ref()
- .and_then(|thread_store| thread_store.upgrade())
- else {
- return Task::ready(Err(anyhow!("text thread store not available")));
- };
-
- let task = thread_store.update(cx, |this, cx| {
- this.load_text_thread(thread.path.clone(), cx)
- });
- cx.spawn(async move |this, cx| {
- let thread = task.await?;
- context_store.update(cx, |context_store, cx| {
- context_store.add_text_thread(thread, true, cx);
- })?;
- this.update(cx, |_this, cx| cx.notify())
- })
- }
- }
- }
-
- fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
- let Some(workspace) = self.workspace.upgrade() else {
- return vec![];
- };
-
- let Some(context_store) = self.context_store.upgrade() else {
- return vec![];
- };
-
- recent_context_picker_entries_with_store(
- context_store,
- self.thread_store.clone(),
- workspace,
- None,
- cx,
- )
- }
-
- fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
- match &self.mode {
- ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
- ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
- ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
- ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
- ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
- ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
- }
- }
-}
-
-impl EventEmitter<DismissEvent> for ContextPicker {}
-
-impl Focusable for ContextPicker {
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- match &self.mode {
- ContextPickerState::Default(menu) => menu.focus_handle(cx),
- ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
- ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
- ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
- ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
- ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
- }
- }
-}
-
-impl Render for ContextPicker {
- fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
- v_flex()
- .w(px(400.))
- .min_w(px(400.))
- .map(|parent| match &self.mode {
- ContextPickerState::Default(menu) => parent.child(menu.clone()),
- ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
- ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
- ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
- ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
- ContextPickerState::Rules(user_rules_picker) => {
- parent.child(user_rules_picker.clone())
- }
- })
- }
-}
-
-pub(crate) enum RecentEntry {
- File {
- project_path: ProjectPath,
- path_prefix: Arc<RelPath>,
- },
- Thread(HistoryEntry),
-}
-
-pub(crate) fn available_context_picker_entries(
- prompt_store: &Option<WeakEntity<PromptStore>>,
- thread_store: &Option<WeakEntity<HistoryStore>>,
- workspace: &Entity<Workspace>,
- cx: &mut App,
-) -> Vec<ContextPickerEntry> {
- let mut entries = vec![
- ContextPickerEntry::Mode(ContextPickerMode::File),
- ContextPickerEntry::Mode(ContextPickerMode::Symbol),
- ];
-
- let has_selection = workspace
- .read(cx)
- .active_item(cx)
- .and_then(|item| item.downcast::<Editor>())
- .is_some_and(|editor| {
- editor.update(cx, |editor, cx| {
- editor.has_non_empty_selection(&editor.display_snapshot(cx))
- })
- });
- if has_selection {
- entries.push(ContextPickerEntry::Action(
- ContextPickerAction::AddSelections,
- ));
- }
-
- if thread_store.is_some() {
- entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
- }
-
- if prompt_store.is_some() {
- entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
- }
-
- entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
-
- entries
-}
-
-fn recent_context_picker_entries_with_store(
- context_store: Entity<ContextStore>,
- thread_store: Option<WeakEntity<HistoryStore>>,
- workspace: Entity<Workspace>,
- exclude_path: Option<ProjectPath>,
- cx: &App,
-) -> Vec<RecentEntry> {
- let project = workspace.read(cx).project();
-
- let mut exclude_paths = context_store.read(cx).file_paths(cx);
- exclude_paths.extend(exclude_path);
-
- let exclude_paths = exclude_paths
- .into_iter()
- .filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx))
- .collect();
-
- let exclude_threads = context_store.read(cx).thread_ids();
-
- recent_context_picker_entries(thread_store, workspace, &exclude_paths, exclude_threads, cx)
-}
-
-pub(crate) fn recent_context_picker_entries(
- thread_store: Option<WeakEntity<HistoryStore>>,
- workspace: Entity<Workspace>,
- exclude_paths: &HashSet<PathBuf>,
- exclude_threads: &HashSet<acp::SessionId>,
- cx: &App,
-) -> Vec<RecentEntry> {
- let mut recent = Vec::with_capacity(6);
- let workspace = workspace.read(cx);
- let project = workspace.project().read(cx);
- let include_root_name = workspace.visible_worktrees(cx).count() > 1;
-
- recent.extend(
- workspace
- .recent_navigation_history_iter(cx)
- .filter(|(_, abs_path)| {
- abs_path
- .as_ref()
- .is_none_or(|path| !exclude_paths.contains(path.as_path()))
- })
- .take(4)
- .filter_map(|(project_path, _)| {
- project
- .worktree_for_id(project_path.worktree_id, cx)
- .map(|worktree| {
- let path_prefix = if include_root_name {
- worktree.read(cx).root_name().into()
- } else {
- RelPath::empty().into()
- };
- RecentEntry::File {
- project_path,
- path_prefix,
- }
- })
- }),
- );
-
- if let Some(thread_store) = thread_store.and_then(|store| store.upgrade()) {
- const RECENT_THREADS_COUNT: usize = 2;
- recent.extend(
- thread_store
- .read(cx)
- .recently_opened_entries(cx)
- .iter()
- .filter(|e| match e.id() {
- HistoryEntryId::AcpThread(session_id) => !exclude_threads.contains(&session_id),
- HistoryEntryId::TextThread(path) => {
- !exclude_paths.contains(&path.to_path_buf())
- }
- })
- .take(RECENT_THREADS_COUNT)
- .map(|thread| RecentEntry::Thread(thread.clone())),
- );
- }
-
- recent
-}
-
-fn add_selections_as_context(
- context_store: &Entity<ContextStore>,
- workspace: &Entity<Workspace>,
- cx: &mut App,
-) {
- let selection_ranges = selection_ranges(workspace, cx);
- context_store.update(cx, |context_store, cx| {
- for (buffer, range) in selection_ranges {
- context_store.add_selection(buffer, range, cx);
- }
- })
-}
-
-pub(crate) fn selection_ranges(
- workspace: &Entity<Workspace>,
- cx: &mut App,
-) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
- let Some(editor) = workspace
- .read(cx)
- .active_item(cx)
- .and_then(|item| item.act_as::<Editor>(cx))
- else {
- return Vec::new();
- };
-
- editor.update(cx, |editor, cx| {
- let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
-
- let buffer = editor.buffer().clone().read(cx);
- let snapshot = buffer.snapshot(cx);
-
- selections
- .into_iter()
- .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
- .flat_map(|range| {
- let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
- let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
- if start_buffer != end_buffer {
- return None;
- }
- Some((start_buffer, start..end))
- })
- .collect::<Vec<_>>()
- })
-}
-
-pub(crate) fn insert_crease_for_mention(
- excerpt_id: ExcerptId,
- crease_start: text::Anchor,
- content_len: usize,
- crease_label: SharedString,
- crease_icon_path: SharedString,
- editor_entity: Entity<Editor>,
- window: &mut Window,
- cx: &mut App,
-) -> Option<CreaseId> {
- editor_entity.update(cx, |editor, cx| {
- let snapshot = editor.buffer().read(cx).snapshot(cx);
-
- let start = snapshot.anchor_in_excerpt(excerpt_id, crease_start)?;
-
- let start = start.bias_right(&snapshot);
- let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
-
- let crease = crease_for_mention(
- crease_label,
- crease_icon_path,
- start..end,
- editor_entity.downgrade(),
- );
-
- let ids = editor.insert_creases(vec![crease.clone()], cx);
- editor.fold_creases(vec![crease], false, window, cx);
-
- Some(ids[0])
- })
-}
-
-pub fn crease_for_mention(
- label: SharedString,
- icon_path: SharedString,
- range: Range<Anchor>,
- editor_entity: WeakEntity<Editor>,
-) -> Crease<Anchor> {
- let placeholder = FoldPlaceholder {
- render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
- merge_adjacent: false,
- ..Default::default()
- };
-
- let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
-
- Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
- .with_metadata(CreaseMetadata { icon_path, label })
-}
-
-fn render_fold_icon_button(
- icon_path: SharedString,
- label: SharedString,
- editor: WeakEntity<Editor>,
-) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
- Arc::new({
- move |fold_id, fold_range, cx| {
- let is_in_text_selection = editor
- .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
- .unwrap_or_default();
-
- ButtonLike::new(fold_id)
- .style(ButtonStyle::Filled)
- .selected_style(ButtonStyle::Tinted(TintColor::Accent))
- .toggle_state(is_in_text_selection)
- .child(
- h_flex()
- .gap_1()
- .child(
- Icon::from_path(icon_path.clone())
- .size(IconSize::XSmall)
- .color(Color::Muted),
- )
- .child(
- Label::new(label.clone())
- .size(LabelSize::Small)
- .buffer_font(cx)
- .single_line(),
- ),
- )
- .into_any_element()
- }
- })
-}
-
-fn fold_toggle(
- name: &'static str,
-) -> impl Fn(
- MultiBufferRow,
- bool,
- Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
- &mut Window,
- &mut App,
-) -> AnyElement {
- move |row, is_folded, fold, _window, _cx| {
- Disclosure::new((name, row.0 as u64), !is_folded)
- .toggle_state(is_folded)
- .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
- .into_any_element()
- }
-}
-
-pub struct MentionLink;
-
-impl MentionLink {
- const FILE: &str = "@file";
- const SYMBOL: &str = "@symbol";
- const SELECTION: &str = "@selection";
- const THREAD: &str = "@thread";
- const FETCH: &str = "@fetch";
- const RULE: &str = "@rule";
-
- const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
-
- pub fn for_file(file_name: &str, full_path: &str) -> String {
- format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
- }
-
- pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
- format!(
- "[@{}]({}:{}:{})",
- symbol_name,
- Self::SYMBOL,
- full_path,
- symbol_name
- )
- }
-
- pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
- format!(
- "[@{} ({}-{})]({}:{}:{}-{})",
- file_name,
- line_range.start + 1,
- line_range.end + 1,
- Self::SELECTION,
- full_path,
- line_range.start,
- line_range.end
- )
- }
-
- pub fn for_thread(thread: &HistoryEntry) -> String {
- match thread {
- HistoryEntry::AcpThread(thread) => {
- format!("[@{}]({}:{})", thread.title, Self::THREAD, thread.id)
- }
- HistoryEntry::TextThread(thread) => {
- let filename = thread
- .path
- .file_name()
- .unwrap_or_default()
- .to_string_lossy();
- let escaped_filename = urlencoding::encode(&filename);
- format!(
- "[@{}]({}:{}{})",
- thread.title,
- Self::THREAD,
- Self::TEXT_THREAD_URL_PREFIX,
- escaped_filename
- )
- }
- }
- }
-
- pub fn for_fetch(url: &str) -> String {
- format!("[@{}]({}:{})", url, Self::FETCH, url)
- }
-
- pub fn for_rule(rule: &RulesContextEntry) -> String {
- format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
- }
-}
@@ -1,252 +0,0 @@
-use std::cell::RefCell;
-use std::rc::Rc;
-use std::sync::Arc;
-
-use anyhow::{Context as _, Result, bail};
-use futures::AsyncReadExt as _;
-use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
-use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
-use http_client::{AsyncBody, HttpClientWithUrl};
-use picker::{Picker, PickerDelegate};
-use ui::{Context, ListItem, Window, prelude::*};
-use workspace::Workspace;
-
-use crate::{context_picker::ContextPicker, context_store::ContextStore};
-
-pub struct FetchContextPicker {
- picker: Entity<Picker<FetchContextPickerDelegate>>,
-}
-
-impl FetchContextPicker {
- pub fn new(
- context_picker: WeakEntity<ContextPicker>,
- workspace: WeakEntity<Workspace>,
- context_store: WeakEntity<ContextStore>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
- let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
-
- Self { picker }
- }
-}
-
-impl Focusable for FetchContextPicker {
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- self.picker.focus_handle(cx)
- }
-}
-
-impl Render for FetchContextPicker {
- fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
- self.picker.clone()
- }
-}
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-enum ContentType {
- Html,
- Plaintext,
- Json,
-}
-
-pub struct FetchContextPickerDelegate {
- context_picker: WeakEntity<ContextPicker>,
- workspace: WeakEntity<Workspace>,
- context_store: WeakEntity<ContextStore>,
- url: String,
-}
-
-impl FetchContextPickerDelegate {
- pub fn new(
- context_picker: WeakEntity<ContextPicker>,
- workspace: WeakEntity<Workspace>,
- context_store: WeakEntity<ContextStore>,
- ) -> Self {
- FetchContextPickerDelegate {
- context_picker,
- workspace,
- context_store,
- url: String::new(),
- }
- }
-}
-
-pub(crate) async fn fetch_url_content(
- http_client: Arc<HttpClientWithUrl>,
- url: String,
-) -> Result<String> {
- let url = if !url.starts_with("https://") && !url.starts_with("http://") {
- format!("https://{url}")
- } else {
- url
- };
-
- let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
-
- let mut body = Vec::new();
- response
- .body_mut()
- .read_to_end(&mut body)
- .await
- .context("error reading response body")?;
-
- if response.status().is_client_error() {
- let text = String::from_utf8_lossy(body.as_slice());
- bail!(
- "status error {}, response: {text:?}",
- response.status().as_u16()
- );
- }
-
- let Some(content_type) = response.headers().get("content-type") else {
- bail!("missing Content-Type header");
- };
- let content_type = content_type
- .to_str()
- .context("invalid Content-Type header")?;
- let content_type = match content_type {
- "text/html" => ContentType::Html,
- "text/plain" => ContentType::Plaintext,
- "application/json" => ContentType::Json,
- _ => ContentType::Html,
- };
-
- match content_type {
- ContentType::Html => {
- let mut handlers: Vec<TagHandler> = vec![
- Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
- Rc::new(RefCell::new(markdown::ParagraphHandler)),
- Rc::new(RefCell::new(markdown::HeadingHandler)),
- Rc::new(RefCell::new(markdown::ListHandler)),
- Rc::new(RefCell::new(markdown::TableHandler::new())),
- Rc::new(RefCell::new(markdown::StyledTextHandler)),
- ];
- if url.contains("wikipedia.org") {
- use html_to_markdown::structure::wikipedia;
-
- handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
- handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
- handlers.push(Rc::new(
- RefCell::new(wikipedia::WikipediaCodeHandler::new()),
- ));
- } else {
- handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
- }
-
- convert_html_to_markdown(&body[..], &mut handlers)
- }
- ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
- ContentType::Json => {
- let json: serde_json::Value = serde_json::from_slice(&body)?;
-
- Ok(format!(
- "```json\n{}\n```",
- serde_json::to_string_pretty(&json)?
- ))
- }
- }
-}
-
-impl PickerDelegate for FetchContextPickerDelegate {
- type ListItem = ListItem;
-
- fn match_count(&self) -> usize {
- if self.url.is_empty() { 0 } else { 1 }
- }
-
- fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
- Some("Enter the URL that you would like to fetch".into())
- }
-
- fn selected_index(&self) -> usize {
- 0
- }
-
- fn set_selected_index(
- &mut self,
- _ix: usize,
- _window: &mut Window,
- _cx: &mut Context<Picker<Self>>,
- ) {
- }
-
- fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
- "Enter a URLβ¦".into()
- }
-
- fn update_matches(
- &mut self,
- query: String,
- _window: &mut Window,
- _cx: &mut Context<Picker<Self>>,
- ) -> Task<()> {
- self.url = query;
-
- Task::ready(())
- }
-
- fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
- let Some(workspace) = self.workspace.upgrade() else {
- return;
- };
-
- let http_client = workspace.read(cx).client().http_client();
- let url = self.url.clone();
- cx.spawn_in(window, async move |this, cx| {
- let text = cx
- .background_spawn(fetch_url_content(http_client, url.clone()))
- .await?;
-
- this.update(cx, |this, cx| {
- this.delegate.context_store.update(cx, |context_store, cx| {
- context_store.add_fetched_url(url, text, cx)
- })
- })??;
-
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
- }
-
- fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
- self.context_picker
- .update(cx, |_, cx| {
- cx.emit(DismissEvent);
- })
- .ok();
- }
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- _window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let added = self
- .context_store
- .upgrade()
- .is_some_and(|context_store| context_store.read(cx).includes_url(&self.url));
-
- Some(
- ListItem::new(ix)
- .inset(true)
- .toggle_state(selected)
- .child(Label::new(self.url.clone()))
- .when(added, |child| {
- child.disabled(true).end_slot(
- h_flex()
- .gap_1()
- .child(
- Icon::new(IconName::Check)
- .size(IconSize::Small)
- .color(Color::Success),
- )
- .child(Label::new("Added").size(LabelSize::Small)),
- )
- }),
- )
- }
-}
@@ -1,392 +0,0 @@
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use file_icons::FileIcons;
-use fuzzy::PathMatch;
-use gpui::{
- App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
-};
-use picker::{Picker, PickerDelegate};
-use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
-use ui::{ListItem, Tooltip, prelude::*};
-use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
-use workspace::Workspace;
-
-use crate::{
- context_picker::ContextPicker,
- context_store::{ContextStore, FileInclusion},
-};
-
-pub struct FileContextPicker {
- picker: Entity<Picker<FileContextPickerDelegate>>,
-}
-
-impl FileContextPicker {
- pub fn new(
- context_picker: WeakEntity<ContextPicker>,
- workspace: WeakEntity<Workspace>,
- context_store: WeakEntity<ContextStore>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
- let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
-
- Self { picker }
- }
-}
-
-impl Focusable for FileContextPicker {
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- self.picker.focus_handle(cx)
- }
-}
-
-impl Render for FileContextPicker {
- fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
- self.picker.clone()
- }
-}
-
-pub struct FileContextPickerDelegate {
- context_picker: WeakEntity<ContextPicker>,
- workspace: WeakEntity<Workspace>,
- context_store: WeakEntity<ContextStore>,
- matches: Vec<FileMatch>,
- selected_index: usize,
-}
-
-impl FileContextPickerDelegate {
- pub fn new(
- context_picker: WeakEntity<ContextPicker>,
- workspace: WeakEntity<Workspace>,
- context_store: WeakEntity<ContextStore>,
- ) -> Self {
- Self {
- context_picker,
- workspace,
- context_store,
- matches: Vec::new(),
- selected_index: 0,
- }
- }
-}
-
-impl PickerDelegate for FileContextPickerDelegate {
- type ListItem = ListItem;
-
- fn match_count(&self) -> usize {
- self.matches.len()
- }
-
- fn selected_index(&self) -> usize {
- self.selected_index
- }
-
- fn set_selected_index(
- &mut self,
- ix: usize,
- _window: &mut Window,
- _cx: &mut Context<Picker<Self>>,
- ) {
- self.selected_index = ix;
- }
-
- fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
- "Search files & directoriesβ¦".into()
- }
-
- fn update_matches(
- &mut self,
- query: String,
- window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Task<()> {
- let Some(workspace) = self.workspace.upgrade() else {
- return Task::ready(());
- };
-
- let search_task = search_files(query, Arc::<AtomicBool>::default(), &workspace, cx);
-
- cx.spawn_in(window, async move |this, cx| {
- // TODO: This should be probably be run in the background.
- let paths = search_task.await;
-
- this.update(cx, |this, _cx| {
- this.delegate.matches = paths;
- })
- .log_err();
- })
- }
-
- fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
- let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
- return;
- };
-
- let project_path = ProjectPath {
- worktree_id: WorktreeId::from_usize(mat.worktree_id),
- path: mat.path.clone(),
- };
-
- let is_directory = mat.is_dir;
-
- self.context_store
- .update(cx, |context_store, cx| {
- if is_directory {
- context_store
- .add_directory(&project_path, true, cx)
- .log_err();
- } else {
- context_store
- .add_file_from_path(project_path.clone(), true, cx)
- .detach_and_log_err(cx);
- }
- })
- .ok();
- }
-
- fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
- self.context_picker
- .update(cx, |_, cx| {
- cx.emit(DismissEvent);
- })
- .ok();
- }
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- _window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let FileMatch { mat, .. } = &self.matches.get(ix)?;
- let workspace = self.workspace.upgrade()?;
- let path_style = workspace.read(cx).path_style(cx);
-
- Some(
- ListItem::new(ix)
- .inset(true)
- .toggle_state(selected)
- .child(render_file_context_entry(
- ElementId::named_usize("file-ctx-picker", ix),
- WorktreeId::from_usize(mat.worktree_id),
- &mat.path,
- &mat.path_prefix,
- mat.is_dir,
- path_style,
- self.context_store.clone(),
- cx,
- )),
- )
- }
-}
-
-pub struct FileMatch {
- pub mat: PathMatch,
- pub is_recent: bool,
-}
-
-pub(crate) fn search_files(
- query: String,
- cancellation_flag: Arc<AtomicBool>,
- workspace: &Entity<Workspace>,
- cx: &App,
-) -> Task<Vec<FileMatch>> {
- if query.is_empty() {
- let workspace = workspace.read(cx);
- let project = workspace.project().read(cx);
- let visible_worktrees = workspace.visible_worktrees(cx).collect::<Vec<_>>();
- let include_root_name = visible_worktrees.len() > 1;
-
- let recent_matches = workspace
- .recent_navigation_history(Some(10), cx)
- .into_iter()
- .map(|(project_path, _)| {
- let path_prefix = if include_root_name {
- project
- .worktree_for_id(project_path.worktree_id, cx)
- .map(|wt| wt.read(cx).root_name().into())
- .unwrap_or_else(|| RelPath::empty().into())
- } else {
- RelPath::empty().into()
- };
-
- FileMatch {
- mat: PathMatch {
- score: 0.,
- positions: Vec::new(),
- worktree_id: project_path.worktree_id.to_usize(),
- path: project_path.path,
- path_prefix,
- distance_to_relative_ancestor: 0,
- is_dir: false,
- },
- is_recent: true,
- }
- });
-
- let file_matches = visible_worktrees.into_iter().flat_map(|worktree| {
- let worktree = worktree.read(cx);
- let path_prefix: Arc<RelPath> = if include_root_name {
- worktree.root_name().into()
- } else {
- RelPath::empty().into()
- };
- worktree.entries(false, 0).map(move |entry| FileMatch {
- mat: PathMatch {
- score: 0.,
- positions: Vec::new(),
- worktree_id: worktree.id().to_usize(),
- path: entry.path.clone(),
- path_prefix: path_prefix.clone(),
- distance_to_relative_ancestor: 0,
- is_dir: entry.is_dir(),
- },
- is_recent: false,
- })
- });
-
- Task::ready(recent_matches.chain(file_matches).collect())
- } else {
- let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
- let include_root_name = worktrees.len() > 1;
- let candidate_sets = worktrees
- .into_iter()
- .map(|worktree| {
- let worktree = worktree.read(cx);
-
- PathMatchCandidateSet {
- snapshot: worktree.snapshot(),
- include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored),
- include_root_name,
- candidates: project::Candidates::Entries,
- }
- })
- .collect::<Vec<_>>();
-
- let executor = cx.background_executor().clone();
- cx.foreground_executor().spawn(async move {
- fuzzy::match_path_sets(
- candidate_sets.as_slice(),
- query.as_str(),
- &None,
- false,
- 100,
- &cancellation_flag,
- executor,
- )
- .await
- .into_iter()
- .map(|mat| FileMatch {
- mat,
- is_recent: false,
- })
- .collect::<Vec<_>>()
- })
- }
-}
-
-pub fn extract_file_name_and_directory(
- path: &RelPath,
- path_prefix: &RelPath,
- path_style: PathStyle,
-) -> (SharedString, Option<SharedString>) {
- // If path is empty, this means we're matching with the root directory itself
- // so we use the path_prefix as the name
- if path.is_empty() && !path_prefix.is_empty() {
- return (path_prefix.display(path_style).to_string().into(), None);
- }
-
- let full_path = path_prefix.join(path);
- let file_name = full_path.file_name().unwrap_or_default();
- let display_path = full_path.display(path_style);
- let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
- (
- file_name.to_string().into(),
- Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
- )
-}
-
-pub fn render_file_context_entry(
- id: ElementId,
- worktree_id: WorktreeId,
- path: &Arc<RelPath>,
- path_prefix: &Arc<RelPath>,
- is_directory: bool,
- path_style: PathStyle,
- context_store: WeakEntity<ContextStore>,
- cx: &App,
-) -> Stateful<Div> {
- let (file_name, directory) = extract_file_name_and_directory(path, path_prefix, path_style);
-
- let added = context_store.upgrade().and_then(|context_store| {
- let project_path = ProjectPath {
- worktree_id,
- path: path.clone(),
- };
- if is_directory {
- context_store
- .read(cx)
- .path_included_in_directory(&project_path, cx)
- } else {
- context_store.read(cx).file_path_included(&project_path, cx)
- }
- });
-
- let file_icon = if is_directory {
- FileIcons::get_folder_icon(false, path.as_std_path(), cx)
- } else {
- FileIcons::get_icon(path.as_std_path(), cx)
- }
- .map(Icon::from_path)
- .unwrap_or_else(|| Icon::new(IconName::File));
-
- h_flex()
- .id(id)
- .gap_1p5()
- .w_full()
- .child(file_icon.size(IconSize::Small).color(Color::Muted))
- .child(
- h_flex()
- .gap_1()
- .child(Label::new(file_name))
- .children(directory.map(|directory| {
- Label::new(directory)
- .size(LabelSize::Small)
- .color(Color::Muted)
- })),
- )
- .when_some(added, |el, added| match added {
- FileInclusion::Direct => el.child(
- h_flex()
- .w_full()
- .justify_end()
- .gap_0p5()
- .child(
- Icon::new(IconName::Check)
- .size(IconSize::Small)
- .color(Color::Success),
- )
- .child(Label::new("Added").size(LabelSize::Small)),
- ),
- FileInclusion::InDirectory { full_path } => {
- let directory_full_path = full_path.to_string_lossy().into_owned();
-
- el.child(
- h_flex()
- .w_full()
- .justify_end()
- .gap_0p5()
- .child(
- Icon::new(IconName::Check)
- .size(IconSize::Small)
- .color(Color::Success),
- )
- .child(Label::new("Included").size(LabelSize::Small)),
- )
- .tooltip(Tooltip::text(format!("in {directory_full_path}")))
- }
- })
-}
@@ -1,224 +0,0 @@
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
-use picker::{Picker, PickerDelegate};
-use prompt_store::{PromptId, PromptStore, UserPromptId};
-use ui::{ListItem, prelude::*};
-use util::ResultExt as _;
-
-use crate::{
- context::RULES_ICON,
- context_picker::ContextPicker,
- context_store::{self, ContextStore},
-};
-
-pub struct RulesContextPicker {
- picker: Entity<Picker<RulesContextPickerDelegate>>,
-}
-
-impl RulesContextPicker {
- pub fn new(
- prompt_store: WeakEntity<PromptStore>,
- context_picker: WeakEntity<ContextPicker>,
- context_store: WeakEntity<context_store::ContextStore>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let delegate = RulesContextPickerDelegate::new(prompt_store, context_picker, context_store);
- let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
-
- RulesContextPicker { picker }
- }
-}
-
-impl Focusable for RulesContextPicker {
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- self.picker.focus_handle(cx)
- }
-}
-
-impl Render for RulesContextPicker {
- fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
- self.picker.clone()
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct RulesContextEntry {
- pub prompt_id: UserPromptId,
- pub title: SharedString,
-}
-
-pub struct RulesContextPickerDelegate {
- prompt_store: WeakEntity<PromptStore>,
- context_picker: WeakEntity<ContextPicker>,
- context_store: WeakEntity<context_store::ContextStore>,
- matches: Vec<RulesContextEntry>,
- selected_index: usize,
-}
-
-impl RulesContextPickerDelegate {
- pub fn new(
- prompt_store: WeakEntity<PromptStore>,
- context_picker: WeakEntity<ContextPicker>,
- context_store: WeakEntity<context_store::ContextStore>,
- ) -> Self {
- RulesContextPickerDelegate {
- prompt_store,
- context_picker,
- context_store,
- matches: Vec::new(),
- selected_index: 0,
- }
- }
-}
-
-impl PickerDelegate for RulesContextPickerDelegate {
- type ListItem = ListItem;
-
- fn match_count(&self) -> usize {
- self.matches.len()
- }
-
- fn selected_index(&self) -> usize {
- self.selected_index
- }
-
- fn set_selected_index(
- &mut self,
- ix: usize,
- _window: &mut Window,
- _cx: &mut Context<Picker<Self>>,
- ) {
- self.selected_index = ix;
- }
-
- fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
- "Search available rulesβ¦".into()
- }
-
- fn update_matches(
- &mut self,
- query: String,
- window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Task<()> {
- let Some(prompt_store) = self.prompt_store.upgrade() else {
- return Task::ready(());
- };
- let search_task = search_rules(query, Arc::new(AtomicBool::default()), &prompt_store, cx);
- cx.spawn_in(window, async move |this, cx| {
- let matches = search_task.await;
- this.update(cx, |this, cx| {
- this.delegate.matches = matches;
- this.delegate.selected_index = 0;
- cx.notify();
- })
- .ok();
- })
- }
-
- fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
- let Some(entry) = self.matches.get(self.selected_index) else {
- return;
- };
-
- self.context_store
- .update(cx, |context_store, cx| {
- context_store.add_rules(entry.prompt_id, true, cx)
- })
- .log_err();
- }
-
- fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
- self.context_picker
- .update(cx, |_, cx| {
- cx.emit(DismissEvent);
- })
- .ok();
- }
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- _window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let thread = &self.matches.get(ix)?;
-
- Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
- render_thread_context_entry(thread, self.context_store.clone(), cx),
- ))
- }
-}
-
-pub fn render_thread_context_entry(
- user_rules: &RulesContextEntry,
- context_store: WeakEntity<ContextStore>,
- cx: &mut App,
-) -> Div {
- let added = context_store.upgrade().is_some_and(|context_store| {
- context_store
- .read(cx)
- .includes_user_rules(user_rules.prompt_id)
- });
-
- h_flex()
- .gap_1p5()
- .w_full()
- .justify_between()
- .child(
- h_flex()
- .gap_1p5()
- .max_w_72()
- .child(
- Icon::new(RULES_ICON)
- .size(IconSize::XSmall)
- .color(Color::Muted),
- )
- .child(Label::new(user_rules.title.clone()).truncate()),
- )
- .when(added, |el| {
- el.child(
- h_flex()
- .gap_1()
- .child(
- Icon::new(IconName::Check)
- .size(IconSize::Small)
- .color(Color::Success),
- )
- .child(Label::new("Added").size(LabelSize::Small)),
- )
- })
-}
-
-pub(crate) fn search_rules(
- query: String,
- cancellation_flag: Arc<AtomicBool>,
- prompt_store: &Entity<PromptStore>,
- cx: &mut App,
-) -> Task<Vec<RulesContextEntry>> {
- let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
- cx.background_spawn(async move {
- search_task
- .await
- .into_iter()
- .flat_map(|metadata| {
- // Default prompts are filtered out as they are automatically included.
- if metadata.default {
- None
- } else {
- match metadata.id {
- PromptId::EditWorkflow => None,
- PromptId::User { uuid } => Some(RulesContextEntry {
- prompt_id: uuid,
- title: metadata.title?,
- }),
- }
- }
- })
- .collect::<Vec<_>>()
- })
-}
@@ -1,415 +0,0 @@
-use std::cmp::Reverse;
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use anyhow::{Result, anyhow};
-use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::{
- App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
-};
-use ordered_float::OrderedFloat;
-use picker::{Picker, PickerDelegate};
-use project::lsp_store::SymbolLocation;
-use project::{DocumentSymbol, Symbol};
-use ui::{ListItem, prelude::*};
-use util::ResultExt as _;
-use workspace::Workspace;
-
-use crate::{
- context::AgentContextHandle, context_picker::ContextPicker, context_store::ContextStore,
-};
-
-pub struct SymbolContextPicker {
- picker: Entity<Picker<SymbolContextPickerDelegate>>,
-}
-
-impl SymbolContextPicker {
- pub fn new(
- context_picker: WeakEntity<ContextPicker>,
- workspace: WeakEntity<Workspace>,
- context_store: WeakEntity<ContextStore>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let delegate = SymbolContextPickerDelegate::new(context_picker, workspace, context_store);
- let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
-
- Self { picker }
- }
-}
-
-impl Focusable for SymbolContextPicker {
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- self.picker.focus_handle(cx)
- }
-}
-
-impl Render for SymbolContextPicker {
- fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
- self.picker.clone()
- }
-}
-
-pub struct SymbolContextPickerDelegate {
- context_picker: WeakEntity<ContextPicker>,
- workspace: WeakEntity<Workspace>,
- context_store: WeakEntity<ContextStore>,
- matches: Vec<SymbolEntry>,
- selected_index: usize,
-}
-
-impl SymbolContextPickerDelegate {
- pub fn new(
- context_picker: WeakEntity<ContextPicker>,
- workspace: WeakEntity<Workspace>,
- context_store: WeakEntity<ContextStore>,
- ) -> Self {
- Self {
- context_picker,
- workspace,
- context_store,
- matches: Vec::new(),
- selected_index: 0,
- }
- }
-}
-
-impl PickerDelegate for SymbolContextPickerDelegate {
- type ListItem = ListItem;
-
- fn match_count(&self) -> usize {
- self.matches.len()
- }
-
- fn selected_index(&self) -> usize {
- self.selected_index
- }
-
- fn set_selected_index(
- &mut self,
- ix: usize,
- _window: &mut Window,
- _cx: &mut Context<Picker<Self>>,
- ) {
- self.selected_index = ix;
- }
-
- fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
- "Search symbolsβ¦".into()
- }
-
- fn update_matches(
- &mut self,
- query: String,
- window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Task<()> {
- let Some(workspace) = self.workspace.upgrade() else {
- return Task::ready(());
- };
-
- let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
- let context_store = self.context_store.clone();
- cx.spawn_in(window, async move |this, cx| {
- let symbols = search_task.await;
-
- let symbol_entries = context_store
- .read_with(cx, |context_store, cx| {
- compute_symbol_entries(symbols, context_store, cx)
- })
- .log_err()
- .unwrap_or_default();
-
- this.update(cx, |this, _cx| {
- this.delegate.matches = symbol_entries;
- })
- .log_err();
- })
- }
-
- fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
- let Some(mat) = self.matches.get(self.selected_index) else {
- return;
- };
- let Some(workspace) = self.workspace.upgrade() else {
- return;
- };
-
- let add_symbol_task = add_symbol(
- mat.symbol.clone(),
- true,
- workspace,
- self.context_store.clone(),
- cx,
- );
-
- let selected_index = self.selected_index;
- cx.spawn(async move |this, cx| {
- let (_, included) = add_symbol_task.await?;
- this.update(cx, |this, _| {
- if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
- mat.is_included = included;
- }
- })
- })
- .detach_and_log_err(cx);
- }
-
- fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
- self.context_picker
- .update(cx, |_, cx| {
- cx.emit(DismissEvent);
- })
- .ok();
- }
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- _window: &mut Window,
- _: &mut Context<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let mat = &self.matches.get(ix)?;
-
- Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
- render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),
- ))
- }
-}
-
-pub(crate) struct SymbolEntry {
- pub symbol: Symbol,
- pub is_included: bool,
-}
-
-pub(crate) fn add_symbol(
- symbol: Symbol,
- remove_if_exists: bool,
- workspace: Entity<Workspace>,
- context_store: WeakEntity<ContextStore>,
- cx: &mut App,
-) -> Task<Result<(Option<AgentContextHandle>, bool)>> {
- let project = workspace.read(cx).project().clone();
- let open_buffer_task = project.update(cx, |project, cx| {
- let SymbolLocation::InProject(symbol_path) = &symbol.path else {
- return Task::ready(Err(anyhow!("can't add symbol from outside of project")));
- };
- project.open_buffer(symbol_path.clone(), cx)
- });
- cx.spawn(async move |cx| {
- let buffer = open_buffer_task.await?;
- let document_symbols = project
- .update(cx, |project, cx| project.document_symbols(&buffer, cx))?
- .await?;
-
- // Try to find a matching document symbol. Document symbols include
- // not only the symbol itself (e.g. function name), but they also
- // include the context that they contain (e.g. function body).
- let (name, range, enclosing_range) = if let Some(DocumentSymbol {
- name,
- range,
- selection_range,
- ..
- }) =
- find_matching_symbol(&symbol, document_symbols.as_slice())
- {
- (name, selection_range, range)
- } else {
- // If we do not find a matching document symbol, fall back to
- // just the symbol itself
- (symbol.name, symbol.range.clone(), symbol.range)
- };
-
- let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| {
- (
- buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
- buffer.anchor_after(enclosing_range.start)
- ..buffer.anchor_before(enclosing_range.end),
- )
- })?;
-
- context_store.update(cx, move |context_store, cx| {
- context_store.add_symbol(
- buffer,
- name.into(),
- range,
- enclosing_range,
- remove_if_exists,
- cx,
- )
- })
- })
-}
-
-fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option<DocumentSymbol> {
- let mut candidates = candidates.iter();
- let mut candidate = candidates.next()?;
-
- loop {
- if candidate.range.start > symbol.range.end {
- return None;
- }
- if candidate.range.end < symbol.range.start {
- candidate = candidates.next()?;
- continue;
- }
- if candidate.selection_range == symbol.range {
- return Some(candidate.clone());
- }
- if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end {
- candidates = candidate.children.iter();
- candidate = candidates.next()?;
- continue;
- }
- return None;
- }
-}
-
-pub struct SymbolMatch {
- pub symbol: Symbol,
-}
-
-pub(crate) fn search_symbols(
- query: String,
- cancellation_flag: Arc<AtomicBool>,
- workspace: &Entity<Workspace>,
- cx: &mut App,
-) -> Task<Vec<SymbolMatch>> {
- let symbols_task = workspace.update(cx, |workspace, cx| {
- workspace
- .project()
- .update(cx, |project, cx| project.symbols(&query, cx))
- });
- let project = workspace.read(cx).project().clone();
- cx.spawn(async move |cx| {
- let Some(symbols) = symbols_task.await.log_err() else {
- return Vec::new();
- };
- let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
- project
- .update(cx, |project, cx| {
- symbols
- .iter()
- .enumerate()
- .map(|(id, symbol)| {
- StringMatchCandidate::new(id, symbol.label.filter_text())
- })
- .partition(|candidate| match &symbols[candidate.id].path {
- SymbolLocation::InProject(project_path) => project
- .entry_for_path(project_path, cx)
- .is_some_and(|e| !e.is_ignored),
- SymbolLocation::OutsideProject { .. } => false,
- })
- })
- .log_err()
- else {
- return Vec::new();
- };
-
- const MAX_MATCHES: usize = 100;
- let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
- &visible_match_candidates,
- &query,
- false,
- true,
- MAX_MATCHES,
- &cancellation_flag,
- cx.background_executor().clone(),
- ));
- let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
- &external_match_candidates,
- &query,
- false,
- true,
- MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
- &cancellation_flag,
- cx.background_executor().clone(),
- ));
- let sort_key_for_match = |mat: &StringMatch| {
- let symbol = &symbols[mat.candidate_id];
- (Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
- };
-
- visible_matches.sort_unstable_by_key(sort_key_for_match);
- external_matches.sort_unstable_by_key(sort_key_for_match);
- let mut matches = visible_matches;
- matches.append(&mut external_matches);
-
- matches
- .into_iter()
- .map(|mut mat| {
- let symbol = symbols[mat.candidate_id].clone();
- let filter_start = symbol.label.filter_range.start;
- for position in &mut mat.positions {
- *position += filter_start;
- }
- SymbolMatch { symbol }
- })
- .collect()
- })
-}
-
-fn compute_symbol_entries(
- symbols: Vec<SymbolMatch>,
- context_store: &ContextStore,
- cx: &App,
-) -> Vec<SymbolEntry> {
- symbols
- .into_iter()
- .map(|SymbolMatch { symbol, .. }| SymbolEntry {
- is_included: context_store.includes_symbol(&symbol, cx),
- symbol,
- })
- .collect::<Vec<_>>()
-}
-
-pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
- let path = match &entry.symbol.path {
- SymbolLocation::InProject(project_path) => {
- project_path.path.file_name().unwrap_or_default().into()
- }
- SymbolLocation::OutsideProject {
- abs_path,
- signature: _,
- } => abs_path
- .file_name()
- .map(|f| f.to_string_lossy())
- .unwrap_or_default(),
- };
- let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
-
- h_flex()
- .id(id)
- .gap_1p5()
- .w_full()
- .child(
- Icon::new(IconName::Code)
- .size(IconSize::Small)
- .color(Color::Muted),
- )
- .child(
- h_flex()
- .gap_1()
- .child(Label::new(&entry.symbol.name))
- .child(
- Label::new(symbol_location)
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- .when(entry.is_included, |el| {
- el.child(
- h_flex()
- .w_full()
- .justify_end()
- .gap_0p5()
- .child(
- Icon::new(IconName::Check)
- .size(IconSize::Small)
- .color(Color::Success),
- )
- .child(Label::new("Added").size(LabelSize::Small)),
- )
- })
-}
@@ -1,280 +0,0 @@
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use crate::{
- context_picker::ContextPicker,
- context_store::{self, ContextStore},
-};
-use agent::{HistoryEntry, HistoryStore};
-use fuzzy::StringMatchCandidate;
-use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
-use picker::{Picker, PickerDelegate};
-use ui::{ListItem, prelude::*};
-use workspace::Workspace;
-
-pub struct ThreadContextPicker {
- picker: Entity<Picker<ThreadContextPickerDelegate>>,
-}
-
-impl ThreadContextPicker {
- pub fn new(
- thread_store: WeakEntity<HistoryStore>,
- context_picker: WeakEntity<ContextPicker>,
- context_store: WeakEntity<context_store::ContextStore>,
- workspace: WeakEntity<Workspace>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let delegate = ThreadContextPickerDelegate::new(
- thread_store,
- context_picker,
- context_store,
- workspace,
- );
- let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
-
- ThreadContextPicker { picker }
- }
-}
-
-impl Focusable for ThreadContextPicker {
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- self.picker.focus_handle(cx)
- }
-}
-
-impl Render for ThreadContextPicker {
- fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
- self.picker.clone()
- }
-}
-
-pub struct ThreadContextPickerDelegate {
- thread_store: WeakEntity<HistoryStore>,
- context_picker: WeakEntity<ContextPicker>,
- context_store: WeakEntity<context_store::ContextStore>,
- workspace: WeakEntity<Workspace>,
- matches: Vec<HistoryEntry>,
- selected_index: usize,
-}
-
-impl ThreadContextPickerDelegate {
- pub fn new(
- thread_store: WeakEntity<HistoryStore>,
- context_picker: WeakEntity<ContextPicker>,
- context_store: WeakEntity<context_store::ContextStore>,
- workspace: WeakEntity<Workspace>,
- ) -> Self {
- ThreadContextPickerDelegate {
- thread_store,
- context_picker,
- context_store,
- workspace,
- matches: Vec::new(),
- selected_index: 0,
- }
- }
-}
-
-impl PickerDelegate for ThreadContextPickerDelegate {
- type ListItem = ListItem;
-
- fn match_count(&self) -> usize {
- self.matches.len()
- }
-
- fn selected_index(&self) -> usize {
- self.selected_index
- }
-
- fn set_selected_index(
- &mut self,
- ix: usize,
- _window: &mut Window,
- _cx: &mut Context<Picker<Self>>,
- ) {
- self.selected_index = ix;
- }
-
- fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
- "Search threadsβ¦".into()
- }
-
- fn update_matches(
- &mut self,
- query: String,
- window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Task<()> {
- let Some(thread_store) = self.thread_store.upgrade() else {
- return Task::ready(());
- };
-
- let search_task = search_threads(query, Arc::new(AtomicBool::default()), &thread_store, cx);
- cx.spawn_in(window, async move |this, cx| {
- let matches = search_task.await;
- this.update(cx, |this, cx| {
- this.delegate.matches = matches;
- this.delegate.selected_index = 0;
- cx.notify();
- })
- .ok();
- })
- }
-
- fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
- let Some(project) = self
- .workspace
- .upgrade()
- .map(|w| w.read(cx).project().clone())
- else {
- return;
- };
- let Some((entry, thread_store)) = self
- .matches
- .get(self.selected_index)
- .zip(self.thread_store.upgrade())
- else {
- return;
- };
-
- match entry {
- HistoryEntry::AcpThread(thread) => {
- let load_thread_task =
- agent::load_agent_thread(thread.id.clone(), thread_store, project, cx);
-
- cx.spawn(async move |this, cx| {
- let thread = load_thread_task.await?;
- this.update(cx, |this, cx| {
- this.delegate
- .context_store
- .update(cx, |context_store, cx| {
- context_store.add_thread(thread, true, cx)
- })
- .ok();
- })
- })
- .detach_and_log_err(cx);
- }
- HistoryEntry::TextThread(thread) => {
- let task = thread_store.update(cx, |this, cx| {
- this.load_text_thread(thread.path.clone(), cx)
- });
-
- cx.spawn(async move |this, cx| {
- let thread = task.await?;
- this.update(cx, |this, cx| {
- this.delegate
- .context_store
- .update(cx, |context_store, cx| {
- context_store.add_text_thread(thread, true, cx)
- })
- .ok();
- })
- })
- .detach_and_log_err(cx);
- }
- }
- }
-
- fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
- self.context_picker
- .update(cx, |_, cx| {
- cx.emit(DismissEvent);
- })
- .ok();
- }
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- _window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let thread = &self.matches.get(ix)?;
-
- Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
- render_thread_context_entry(thread, self.context_store.clone(), cx),
- ))
- }
-}
-
-pub fn render_thread_context_entry(
- entry: &HistoryEntry,
- context_store: WeakEntity<ContextStore>,
- cx: &mut App,
-) -> Div {
- let is_added = match entry {
- HistoryEntry::AcpThread(thread) => context_store
- .upgrade()
- .is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(&thread.id)),
- HistoryEntry::TextThread(thread) => context_store
- .upgrade()
- .is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(&thread.path)),
- };
-
- h_flex()
- .gap_1p5()
- .w_full()
- .justify_between()
- .child(
- h_flex()
- .gap_1p5()
- .max_w_72()
- .child(
- Icon::new(IconName::Thread)
- .size(IconSize::XSmall)
- .color(Color::Muted),
- )
- .child(Label::new(entry.title().clone()).truncate()),
- )
- .when(is_added, |el| {
- el.child(
- h_flex()
- .gap_1()
- .child(
- Icon::new(IconName::Check)
- .size(IconSize::Small)
- .color(Color::Success),
- )
- .child(Label::new("Added").size(LabelSize::Small)),
- )
- })
-}
-
-pub(crate) fn search_threads(
- query: String,
- cancellation_flag: Arc<AtomicBool>,
- thread_store: &Entity<HistoryStore>,
- cx: &mut App,
-) -> Task<Vec<HistoryEntry>> {
- let threads = thread_store.read(cx).entries().collect();
- if query.is_empty() {
- return Task::ready(threads);
- }
-
- let executor = cx.background_executor().clone();
- cx.background_spawn(async move {
- let candidates = threads
- .iter()
- .enumerate()
- .map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
- .collect::<Vec<_>>();
- let matches = fuzzy::match_strings(
- &candidates,
- &query,
- false,
- true,
- 100,
- &cancellation_flag,
- executor,
- )
- .await;
-
- matches
- .into_iter()
- .map(|mat| threads[mat.candidate_id].clone())
- .collect()
- })
-}
@@ -1,614 +0,0 @@
-use crate::context::{
- AgentContextHandle, AgentContextKey, ContextId, ContextKind, DirectoryContextHandle,
- FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle,
- SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
-};
-use agent_client_protocol as acp;
-use anyhow::{Context as _, Result, anyhow};
-use assistant_text_thread::TextThread;
-use collections::{HashSet, IndexSet};
-use futures::{self, FutureExt};
-use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
-use language::{Buffer, File as _};
-use language_model::LanguageModelImage;
-use project::{
- Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file,
- lsp_store::SymbolLocation,
-};
-use prompt_store::UserPromptId;
-use ref_cast::RefCast as _;
-use std::{
- ops::Range,
- path::{Path, PathBuf},
- sync::Arc,
-};
-use text::{Anchor, OffsetRangeExt};
-
-pub struct ContextStore {
- project: WeakEntity<Project>,
- next_context_id: ContextId,
- context_set: IndexSet<AgentContextKey>,
- context_thread_ids: HashSet<acp::SessionId>,
- context_text_thread_paths: HashSet<Arc<Path>>,
-}
-
-pub enum ContextStoreEvent {
- ContextRemoved(AgentContextKey),
-}
-
-impl EventEmitter<ContextStoreEvent> for ContextStore {}
-
-impl ContextStore {
- pub fn new(project: WeakEntity<Project>) -> Self {
- Self {
- project,
- next_context_id: ContextId::zero(),
- context_set: IndexSet::default(),
- context_thread_ids: HashSet::default(),
- context_text_thread_paths: HashSet::default(),
- }
- }
-
- pub fn context(&self) -> impl Iterator<Item = &AgentContextHandle> {
- self.context_set.iter().map(|entry| entry.as_ref())
- }
-
- pub fn clear(&mut self, cx: &mut Context<Self>) {
- self.context_set.clear();
- self.context_thread_ids.clear();
- cx.notify();
- }
-
- pub fn add_file_from_path(
- &mut self,
- project_path: ProjectPath,
- remove_if_exists: bool,
- cx: &mut Context<Self>,
- ) -> Task<Result<Option<AgentContextHandle>>> {
- let Some(project) = self.project.upgrade() else {
- return Task::ready(Err(anyhow!("failed to read project")));
- };
-
- if is_image_file(&project, &project_path, cx) {
- self.add_image_from_path(project_path, remove_if_exists, cx)
- } else {
- cx.spawn(async move |this, cx| {
- let open_buffer_task = project.update(cx, |project, cx| {
- project.open_buffer(project_path.clone(), cx)
- })?;
- let buffer = open_buffer_task.await?;
- this.update(cx, |this, cx| {
- this.add_file_from_buffer(&project_path, buffer, remove_if_exists, cx)
- })
- })
- }
- }
-
- pub fn add_file_from_buffer(
- &mut self,
- project_path: &ProjectPath,
- buffer: Entity<Buffer>,
- remove_if_exists: bool,
- cx: &mut Context<Self>,
- ) -> Option<AgentContextHandle> {
- let context_id = self.next_context_id.post_inc();
- let context = AgentContextHandle::File(FileContextHandle { buffer, context_id });
-
- if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
- if remove_if_exists {
- self.remove_context(&context, cx);
- None
- } else {
- Some(key.as_ref().clone())
- }
- } else if self.path_included_in_directory(project_path, cx).is_some() {
- None
- } else {
- self.insert_context(context.clone(), cx);
- Some(context)
- }
- }
-
- pub fn add_directory(
- &mut self,
- project_path: &ProjectPath,
- remove_if_exists: bool,
- cx: &mut Context<Self>,
- ) -> Result<Option<AgentContextHandle>> {
- let project = self.project.upgrade().context("failed to read project")?;
- let entry_id = project
- .read(cx)
- .entry_for_path(project_path, cx)
- .map(|entry| entry.id)
- .context("no entry found for directory context")?;
-
- let context_id = self.next_context_id.post_inc();
- let context = AgentContextHandle::Directory(DirectoryContextHandle {
- entry_id,
- context_id,
- });
-
- let context =
- if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
- if remove_if_exists {
- self.remove_context(&context, cx);
- None
- } else {
- Some(existing.as_ref().clone())
- }
- } else {
- self.insert_context(context.clone(), cx);
- Some(context)
- };
-
- anyhow::Ok(context)
- }
-
- pub fn add_symbol(
- &mut self,
- buffer: Entity<Buffer>,
- symbol: SharedString,
- range: Range<Anchor>,
- enclosing_range: Range<Anchor>,
- remove_if_exists: bool,
- cx: &mut Context<Self>,
- ) -> (Option<AgentContextHandle>, bool) {
- let context_id = self.next_context_id.post_inc();
- let context = AgentContextHandle::Symbol(SymbolContextHandle {
- buffer,
- symbol,
- range,
- enclosing_range,
- context_id,
- });
-
- if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
- let handle = if remove_if_exists {
- self.remove_context(&context, cx);
- None
- } else {
- Some(key.as_ref().clone())
- };
- return (handle, false);
- }
-
- let included = self.insert_context(context.clone(), cx);
- (Some(context), included)
- }
-
- pub fn add_thread(
- &mut self,
- thread: Entity<agent::Thread>,
- remove_if_exists: bool,
- cx: &mut Context<Self>,
- ) -> Option<AgentContextHandle> {
- let context_id = self.next_context_id.post_inc();
- let context = AgentContextHandle::Thread(ThreadContextHandle { thread, context_id });
-
- if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
- if remove_if_exists {
- self.remove_context(&context, cx);
- None
- } else {
- Some(existing.as_ref().clone())
- }
- } else {
- self.insert_context(context.clone(), cx);
- Some(context)
- }
- }
-
- pub fn add_text_thread(
- &mut self,
- text_thread: Entity<TextThread>,
- remove_if_exists: bool,
- cx: &mut Context<Self>,
- ) -> Option<AgentContextHandle> {
- let context_id = self.next_context_id.post_inc();
- let context = AgentContextHandle::TextThread(TextThreadContextHandle {
- text_thread,
- context_id,
- });
-
- if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
- if remove_if_exists {
- self.remove_context(&context, cx);
- None
- } else {
- Some(existing.as_ref().clone())
- }
- } else {
- self.insert_context(context.clone(), cx);
- Some(context)
- }
- }
-
- pub fn add_rules(
- &mut self,
- prompt_id: UserPromptId,
- remove_if_exists: bool,
- cx: &mut Context<ContextStore>,
- ) -> Option<AgentContextHandle> {
- let context_id = self.next_context_id.post_inc();
- let context = AgentContextHandle::Rules(RulesContextHandle {
- prompt_id,
- context_id,
- });
-
- if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
- if remove_if_exists {
- self.remove_context(&context, cx);
- None
- } else {
- Some(existing.as_ref().clone())
- }
- } else {
- self.insert_context(context.clone(), cx);
- Some(context)
- }
- }
-
- pub fn add_fetched_url(
- &mut self,
- url: String,
- text: impl Into<SharedString>,
- cx: &mut Context<ContextStore>,
- ) -> AgentContextHandle {
- let context = AgentContextHandle::FetchedUrl(FetchedUrlContext {
- url: url.into(),
- text: text.into(),
- context_id: self.next_context_id.post_inc(),
- });
-
- self.insert_context(context.clone(), cx);
- context
- }
-
- pub fn add_image_from_path(
- &mut self,
- project_path: ProjectPath,
- remove_if_exists: bool,
- cx: &mut Context<ContextStore>,
- ) -> Task<Result<Option<AgentContextHandle>>> {
- let project = self.project.clone();
- cx.spawn(async move |this, cx| {
- let open_image_task = project.update(cx, |project, cx| {
- project.open_image(project_path.clone(), cx)
- })?;
- let image_item = open_image_task.await?;
-
- this.update(cx, |this, cx| {
- let item = image_item.read(cx);
- this.insert_image(
- Some(item.project_path(cx)),
- Some(item.file.full_path(cx).to_string_lossy().into_owned()),
- item.image.clone(),
- remove_if_exists,
- cx,
- )
- })
- })
- }
-
- pub fn add_image_instance(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) {
- self.insert_image(None, None, image, false, cx);
- }
-
- fn insert_image(
- &mut self,
- project_path: Option<ProjectPath>,
- full_path: Option<String>,
- image: Arc<Image>,
- remove_if_exists: bool,
- cx: &mut Context<ContextStore>,
- ) -> Option<AgentContextHandle> {
- let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
- let context = AgentContextHandle::Image(ImageContext {
- project_path,
- full_path,
- original_image: image,
- image_task,
- context_id: self.next_context_id.post_inc(),
- });
- if self.has_context(&context) && remove_if_exists {
- self.remove_context(&context, cx);
- return None;
- }
-
- self.insert_context(context.clone(), cx);
- Some(context)
- }
-
- pub fn add_selection(
- &mut self,
- buffer: Entity<Buffer>,
- range: Range<Anchor>,
- cx: &mut Context<ContextStore>,
- ) {
- let context_id = self.next_context_id.post_inc();
- let context = AgentContextHandle::Selection(SelectionContextHandle {
- buffer,
- range,
- context_id,
- });
- self.insert_context(context, cx);
- }
-
- pub fn add_suggested_context(
- &mut self,
- suggested: &SuggestedContext,
- cx: &mut Context<ContextStore>,
- ) {
- match suggested {
- SuggestedContext::File {
- buffer,
- icon_path: _,
- name: _,
- } => {
- if let Some(buffer) = buffer.upgrade() {
- let context_id = self.next_context_id.post_inc();
- self.insert_context(
- AgentContextHandle::File(FileContextHandle { buffer, context_id }),
- cx,
- );
- };
- }
- SuggestedContext::TextThread {
- text_thread,
- name: _,
- } => {
- if let Some(text_thread) = text_thread.upgrade() {
- let context_id = self.next_context_id.post_inc();
- self.insert_context(
- AgentContextHandle::TextThread(TextThreadContextHandle {
- text_thread,
- context_id,
- }),
- cx,
- );
- }
- }
- }
- }
-
- fn insert_context(&mut self, context: AgentContextHandle, cx: &mut Context<Self>) -> bool {
- match &context {
- // AgentContextHandle::Thread(thread_context) => {
- // if let Some(thread_store) = self.thread_store.clone() {
- // thread_context.thread.update(cx, |thread, cx| {
- // thread.start_generating_detailed_summary_if_needed(thread_store, cx);
- // });
- // self.context_thread_ids
- // .insert(thread_context.thread.read(cx).id().clone());
- // } else {
- // return false;
- // }
- // }
- AgentContextHandle::TextThread(text_thread_context) => {
- self.context_text_thread_paths
- .extend(text_thread_context.text_thread.read(cx).path().cloned());
- }
- _ => {}
- }
- let inserted = self.context_set.insert(AgentContextKey(context));
- if inserted {
- cx.notify();
- }
- inserted
- }
-
- pub fn remove_context(&mut self, context: &AgentContextHandle, cx: &mut Context<Self>) {
- if let Some((_, key)) = self
- .context_set
- .shift_remove_full(AgentContextKey::ref_cast(context))
- {
- match context {
- AgentContextHandle::Thread(thread_context) => {
- self.context_thread_ids
- .remove(thread_context.thread.read(cx).id());
- }
- AgentContextHandle::TextThread(text_thread_context) => {
- if let Some(path) = text_thread_context.text_thread.read(cx).path() {
- self.context_text_thread_paths.remove(path);
- }
- }
- _ => {}
- }
- cx.emit(ContextStoreEvent::ContextRemoved(key));
- cx.notify();
- }
- }
-
- pub fn has_context(&mut self, context: &AgentContextHandle) -> bool {
- self.context_set
- .contains(AgentContextKey::ref_cast(context))
- }
-
- /// Returns whether this file path is already included directly in the context, or if it will be
- /// included in the context via a directory.
- pub fn file_path_included(&self, path: &ProjectPath, cx: &App) -> Option<FileInclusion> {
- let project = self.project.upgrade()?.read(cx);
- self.context().find_map(|context| match context {
- AgentContextHandle::File(file_context) => {
- FileInclusion::check_file(file_context, path, cx)
- }
- AgentContextHandle::Image(image_context) => {
- FileInclusion::check_image(image_context, path)
- }
- AgentContextHandle::Directory(directory_context) => {
- FileInclusion::check_directory(directory_context, path, project, cx)
- }
- _ => None,
- })
- }
-
- pub fn path_included_in_directory(
- &self,
- path: &ProjectPath,
- cx: &App,
- ) -> Option<FileInclusion> {
- let project = self.project.upgrade()?.read(cx);
- self.context().find_map(|context| match context {
- AgentContextHandle::Directory(directory_context) => {
- FileInclusion::check_directory(directory_context, path, project, cx)
- }
- _ => None,
- })
- }
-
- pub fn includes_symbol(&self, symbol: &Symbol, cx: &App) -> bool {
- self.context().any(|context| match context {
- AgentContextHandle::Symbol(context) => {
- if context.symbol != symbol.name {
- return false;
- }
- let buffer = context.buffer.read(cx);
- let Some(context_path) = buffer.project_path(cx) else {
- return false;
- };
- if symbol.path != SymbolLocation::InProject(context_path) {
- return false;
- }
- let context_range = context.range.to_point_utf16(&buffer.snapshot());
- context_range.start == symbol.range.start.0
- && context_range.end == symbol.range.end.0
- }
- _ => false,
- })
- }
-
- pub fn includes_thread(&self, thread_id: &acp::SessionId) -> bool {
- self.context_thread_ids.contains(thread_id)
- }
-
- pub fn includes_text_thread(&self, path: &Arc<Path>) -> bool {
- self.context_text_thread_paths.contains(path)
- }
-
- pub fn includes_user_rules(&self, prompt_id: UserPromptId) -> bool {
- self.context_set
- .contains(&RulesContextHandle::lookup_key(prompt_id))
- }
-
- pub fn includes_url(&self, url: impl Into<SharedString>) -> bool {
- self.context_set
- .contains(&FetchedUrlContext::lookup_key(url.into()))
- }
-
- pub fn get_url_context(&self, url: SharedString) -> Option<AgentContextHandle> {
- self.context_set
- .get(&FetchedUrlContext::lookup_key(url))
- .map(|key| key.as_ref().clone())
- }
-
- pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
- self.context()
- .filter_map(|context| match context {
- AgentContextHandle::File(file) => {
- let buffer = file.buffer.read(cx);
- buffer.project_path(cx)
- }
- AgentContextHandle::Directory(_)
- | AgentContextHandle::Symbol(_)
- | AgentContextHandle::Thread(_)
- | AgentContextHandle::Selection(_)
- | AgentContextHandle::FetchedUrl(_)
- | AgentContextHandle::TextThread(_)
- | AgentContextHandle::Rules(_)
- | AgentContextHandle::Image(_) => None,
- })
- .collect()
- }
-
- pub fn thread_ids(&self) -> &HashSet<acp::SessionId> {
- &self.context_thread_ids
- }
-}
-
-#[derive(Clone)]
-pub enum SuggestedContext {
- File {
- name: SharedString,
- icon_path: Option<SharedString>,
- buffer: WeakEntity<Buffer>,
- },
- TextThread {
- name: SharedString,
- text_thread: WeakEntity<TextThread>,
- },
-}
-
-impl SuggestedContext {
- pub fn name(&self) -> &SharedString {
- match self {
- Self::File { name, .. } => name,
- Self::TextThread { name, .. } => name,
- }
- }
-
- pub fn icon_path(&self) -> Option<SharedString> {
- match self {
- Self::File { icon_path, .. } => icon_path.clone(),
- Self::TextThread { .. } => None,
- }
- }
-
- pub fn kind(&self) -> ContextKind {
- match self {
- Self::File { .. } => ContextKind::File,
- Self::TextThread { .. } => ContextKind::TextThread,
- }
- }
-}
-
-pub enum FileInclusion {
- Direct,
- InDirectory { full_path: PathBuf },
-}
-
-impl FileInclusion {
- fn check_file(file_context: &FileContextHandle, path: &ProjectPath, cx: &App) -> Option<Self> {
- let file_path = file_context.buffer.read(cx).project_path(cx)?;
- if path == &file_path {
- Some(FileInclusion::Direct)
- } else {
- None
- }
- }
-
- fn check_image(image_context: &ImageContext, path: &ProjectPath) -> Option<Self> {
- let image_path = image_context.project_path.as_ref()?;
- if path == image_path {
- Some(FileInclusion::Direct)
- } else {
- None
- }
- }
-
- fn check_directory(
- directory_context: &DirectoryContextHandle,
- path: &ProjectPath,
- project: &Project,
- cx: &App,
- ) -> Option<Self> {
- let worktree = project
- .worktree_for_entry(directory_context.entry_id, cx)?
- .read(cx);
- let entry = worktree.entry_for_id(directory_context.entry_id)?;
- let directory_path = ProjectPath {
- worktree_id: worktree.id(),
- path: entry.path.clone(),
- };
- if path.starts_with(&directory_path) {
- if path == &directory_path {
- Some(FileInclusion::Direct)
- } else {
- Some(FileInclusion::InDirectory {
- full_path: worktree.full_path(&entry.path),
- })
- }
- } else {
- None
- }
- }
-}
@@ -1,619 +0,0 @@
-use crate::{
- AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
- ModelUsageContext, RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
- context_picker::ContextPicker,
- ui::{AddedContext, ContextPill},
-};
-use crate::{
- context::AgentContextHandle,
- context_store::{ContextStore, SuggestedContext},
-};
-use agent::HistoryStore;
-use collections::HashSet;
-use editor::Editor;
-use gpui::{
- App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
- Subscription, Task, WeakEntity,
-};
-use itertools::Itertools;
-use project::ProjectItem;
-use prompt_store::PromptStore;
-use rope::Point;
-use std::rc::Rc;
-use text::ToPoint as _;
-use ui::{PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
-use util::ResultExt as _;
-use workspace::Workspace;
-use zed_actions::assistant::OpenRulesLibrary;
-
-pub struct ContextStrip {
- context_store: Entity<ContextStore>,
- context_picker: Entity<ContextPicker>,
- context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
- focus_handle: FocusHandle,
- suggest_context_kind: SuggestContextKind,
- workspace: WeakEntity<Workspace>,
- prompt_store: Option<WeakEntity<PromptStore>>,
- _subscriptions: Vec<Subscription>,
- focused_index: Option<usize>,
- children_bounds: Option<Vec<Bounds<Pixels>>>,
- model_usage_context: ModelUsageContext,
-}
-
-impl ContextStrip {
- pub fn new(
- context_store: Entity<ContextStore>,
- workspace: WeakEntity<Workspace>,
- thread_store: Option<WeakEntity<HistoryStore>>,
- prompt_store: Option<WeakEntity<PromptStore>>,
- context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
- suggest_context_kind: SuggestContextKind,
- model_usage_context: ModelUsageContext,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let context_picker = cx.new(|cx| {
- ContextPicker::new(
- workspace.clone(),
- thread_store.clone(),
- prompt_store.clone(),
- context_store.downgrade(),
- window,
- cx,
- )
- });
-
- let focus_handle = cx.focus_handle();
-
- let subscriptions = vec![
- cx.observe(&context_store, |_, _, cx| cx.notify()),
- cx.subscribe_in(&context_picker, window, Self::handle_context_picker_event),
- cx.on_focus(&focus_handle, window, Self::handle_focus),
- cx.on_blur(&focus_handle, window, Self::handle_blur),
- ];
-
- Self {
- context_store: context_store.clone(),
- context_picker,
- context_picker_menu_handle,
- focus_handle,
- suggest_context_kind,
- workspace,
- prompt_store,
- _subscriptions: subscriptions,
- focused_index: None,
- children_bounds: None,
- model_usage_context,
- }
- }
-
- /// Whether or not the context strip has items to display
- pub fn has_context_items(&self, cx: &App) -> bool {
- self.context_store.read(cx).context().next().is_some()
- || self.suggested_context(cx).is_some()
- }
-
- fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
- if let Some(workspace) = self.workspace.upgrade() {
- let project = workspace.read(cx).project().read(cx);
- let prompt_store = self.prompt_store.as_ref().and_then(|p| p.upgrade());
-
- let current_model = self.model_usage_context.language_model(cx);
-
- self.context_store
- .read(cx)
- .context()
- .flat_map(|context| {
- AddedContext::new_pending(
- context.clone(),
- prompt_store.as_ref(),
- project,
- current_model.as_ref(),
- cx,
- )
- })
- .collect::<Vec<_>>()
- } else {
- Vec::new()
- }
- }
-
- fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
- match self.suggest_context_kind {
- SuggestContextKind::Thread => self.suggested_thread(cx),
- }
- }
-
- fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
- if !self.context_picker.read(cx).allow_threads() {
- return None;
- }
-
- let workspace = self.workspace.upgrade()?;
- let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
-
- if let Some(active_text_thread_editor) = panel.active_text_thread_editor() {
- let text_thread = active_text_thread_editor.read(cx).text_thread();
- let weak_text_thread = text_thread.downgrade();
- let text_thread = text_thread.read(cx);
- let path = text_thread.path()?;
-
- if self.context_store.read(cx).includes_text_thread(path) {
- return None;
- }
-
- Some(SuggestedContext::TextThread {
- name: text_thread.summary().or_default(),
- text_thread: weak_text_thread,
- })
- } else {
- None
- }
- }
-
- fn handle_context_picker_event(
- &mut self,
- _picker: &Entity<ContextPicker>,
- _event: &DismissEvent,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- cx.emit(ContextStripEvent::PickerDismissed);
- }
-
- fn handle_focus(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
- self.focused_index = self.last_pill_index();
- cx.notify();
- }
-
- fn handle_blur(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
- self.focused_index = None;
- cx.notify();
- }
-
- fn focus_left(&mut self, _: &FocusLeft, _window: &mut Window, cx: &mut Context<Self>) {
- self.focused_index = match self.focused_index {
- Some(index) if index > 0 => Some(index - 1),
- _ => self.last_pill_index(),
- };
-
- cx.notify();
- }
-
- fn focus_right(&mut self, _: &FocusRight, _window: &mut Window, cx: &mut Context<Self>) {
- let Some(last_index) = self.last_pill_index() else {
- return;
- };
-
- self.focused_index = match self.focused_index {
- Some(index) if index < last_index => Some(index + 1),
- _ => Some(0),
- };
-
- cx.notify();
- }
-
- fn focus_up(&mut self, _: &FocusUp, _window: &mut Window, cx: &mut Context<Self>) {
- let Some(focused_index) = self.focused_index else {
- return;
- };
-
- if focused_index == 0 {
- return cx.emit(ContextStripEvent::BlurredUp);
- }
-
- let Some((focused, pills)) = self.focused_bounds(focused_index) else {
- return;
- };
-
- let iter = pills[..focused_index].iter().enumerate().rev();
- self.focused_index = Self::find_best_horizontal_match(focused, iter).or(Some(0));
- cx.notify();
- }
-
- fn focus_down(&mut self, _: &FocusDown, _window: &mut Window, cx: &mut Context<Self>) {
- let Some(focused_index) = self.focused_index else {
- return;
- };
-
- let last_index = self.last_pill_index();
-
- if self.focused_index == last_index {
- return cx.emit(ContextStripEvent::BlurredDown);
- }
-
- let Some((focused, pills)) = self.focused_bounds(focused_index) else {
- return;
- };
-
- let iter = pills.iter().enumerate().skip(focused_index + 1);
- self.focused_index = Self::find_best_horizontal_match(focused, iter).or(last_index);
- cx.notify();
- }
-
- fn focused_bounds(&self, focused: usize) -> Option<(&Bounds<Pixels>, &[Bounds<Pixels>])> {
- let pill_bounds = self.pill_bounds()?;
- let focused = pill_bounds.get(focused)?;
-
- Some((focused, pill_bounds))
- }
-
- fn pill_bounds(&self) -> Option<&[Bounds<Pixels>]> {
- let bounds = self.children_bounds.as_ref()?;
- let eraser = if bounds.len() < 3 { 0 } else { 1 };
- let pills = &bounds[1..bounds.len() - eraser];
-
- if pills.is_empty() { None } else { Some(pills) }
- }
-
- fn last_pill_index(&self) -> Option<usize> {
- Some(self.pill_bounds()?.len() - 1)
- }
-
- fn find_best_horizontal_match<'a>(
- focused: &'a Bounds<Pixels>,
- iter: impl Iterator<Item = (usize, &'a Bounds<Pixels>)>,
- ) -> Option<usize> {
- let mut best = None;
-
- let focused_left = focused.left();
- let focused_right = focused.right();
-
- for (index, probe) in iter {
- if probe.origin.y == focused.origin.y {
- continue;
- }
-
- let overlap = probe.right().min(focused_right) - probe.left().max(focused_left);
-
- best = match best {
- Some((_, prev_overlap, y)) if probe.origin.y != y || prev_overlap > overlap => {
- break;
- }
- Some(_) | None => Some((index, overlap, probe.origin.y)),
- };
- }
-
- best.map(|(index, _, _)| index)
- }
-
- fn open_context(&mut self, context: &AgentContextHandle, window: &mut Window, cx: &mut App) {
- let Some(workspace) = self.workspace.upgrade() else {
- return;
- };
-
- match context {
- AgentContextHandle::File(file_context) => {
- if let Some(project_path) = file_context.project_path(cx) {
- workspace.update(cx, |workspace, cx| {
- workspace
- .open_path(project_path, None, true, window, cx)
- .detach_and_log_err(cx);
- });
- }
- }
-
- AgentContextHandle::Directory(directory_context) => {
- let entry_id = directory_context.entry_id;
- workspace.update(cx, |workspace, cx| {
- workspace.project().update(cx, |_project, cx| {
- cx.emit(project::Event::RevealInProjectPanel(entry_id));
- })
- })
- }
-
- AgentContextHandle::Symbol(symbol_context) => {
- let buffer = symbol_context.buffer.read(cx);
- if let Some(project_path) = buffer.project_path(cx) {
- let snapshot = buffer.snapshot();
- let target_position = symbol_context.range.start.to_point(&snapshot);
- open_editor_at_position(project_path, target_position, &workspace, window, cx)
- .detach();
- }
- }
-
- AgentContextHandle::Selection(selection_context) => {
- let buffer = selection_context.buffer.read(cx);
- if let Some(project_path) = buffer.project_path(cx) {
- let snapshot = buffer.snapshot();
- let target_position = selection_context.range.start.to_point(&snapshot);
-
- open_editor_at_position(project_path, target_position, &workspace, window, cx)
- .detach();
- }
- }
-
- AgentContextHandle::FetchedUrl(fetched_url_context) => {
- cx.open_url(&fetched_url_context.url);
- }
-
- AgentContextHandle::Thread(_thread_context) => {}
-
- AgentContextHandle::TextThread(text_thread_context) => {
- workspace.update(cx, |workspace, cx| {
- if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
- let context = text_thread_context.text_thread.clone();
- window.defer(cx, move |window, cx| {
- panel.update(cx, |panel, cx| {
- panel.open_text_thread(context, window, cx)
- });
- });
- }
- })
- }
-
- AgentContextHandle::Rules(rules_context) => window.dispatch_action(
- Box::new(OpenRulesLibrary {
- prompt_to_select: Some(rules_context.prompt_id.0),
- }),
- cx,
- ),
-
- AgentContextHandle::Image(_) => {}
- }
- }
-
- fn remove_focused_context(
- &mut self,
- _: &RemoveFocusedContext,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if let Some(index) = self.focused_index {
- let added_contexts = self.added_contexts(cx);
- let Some(context) = added_contexts.get(index) else {
- return;
- };
-
- self.context_store.update(cx, |this, cx| {
- this.remove_context(&context.handle, cx);
- });
-
- let is_now_empty = added_contexts.len() == 1;
- if is_now_empty {
- cx.emit(ContextStripEvent::BlurredEmpty);
- } else {
- self.focused_index = Some(index.saturating_sub(1));
- cx.notify();
- }
- }
- }
-
- fn is_suggested_focused(&self, added_contexts: &Vec<AddedContext>) -> bool {
- // We only suggest one item after the actual context
- self.focused_index == Some(added_contexts.len())
- }
-
- fn accept_suggested_context(
- &mut self,
- _: &AcceptSuggestedContext,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if let Some(suggested) = self.suggested_context(cx)
- && self.is_suggested_focused(&self.added_contexts(cx))
- {
- self.add_suggested_context(&suggested, cx);
- }
- }
-
- fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context<Self>) {
- self.context_store.update(cx, |context_store, cx| {
- context_store.add_suggested_context(suggested, cx)
- });
- cx.notify();
- }
-}
-
-impl Focusable for ContextStrip {
- fn focus_handle(&self, _cx: &App) -> FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl Render for ContextStrip {
- fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let context_picker = self.context_picker.clone();
- let focus_handle = self.focus_handle.clone();
-
- let added_contexts = self.added_contexts(cx);
- let dupe_names = added_contexts
- .iter()
- .map(|c| c.name.clone())
- .sorted()
- .tuple_windows()
- .filter(|(a, b)| a == b)
- .map(|(a, _)| a)
- .collect::<HashSet<SharedString>>();
- let no_added_context = added_contexts.is_empty();
-
- let suggested_context = self.suggested_context(cx).map(|suggested_context| {
- (
- suggested_context,
- self.is_suggested_focused(&added_contexts),
- )
- });
-
- h_flex()
- .flex_wrap()
- .gap_1()
- .track_focus(&focus_handle)
- .key_context("ContextStrip")
- .on_action(cx.listener(Self::focus_up))
- .on_action(cx.listener(Self::focus_right))
- .on_action(cx.listener(Self::focus_down))
- .on_action(cx.listener(Self::focus_left))
- .on_action(cx.listener(Self::remove_focused_context))
- .on_action(cx.listener(Self::accept_suggested_context))
- .on_children_prepainted({
- let entity = cx.entity().downgrade();
- move |children_bounds, _window, cx| {
- entity
- .update(cx, |this, _| {
- this.children_bounds = Some(children_bounds);
- })
- .ok();
- }
- })
- .child(
- PopoverMenu::new("context-picker")
- .menu({
- let context_picker = context_picker.clone();
- move |window, cx| {
- context_picker.update(cx, |this, cx| {
- this.init(window, cx);
- });
-
- Some(context_picker.clone())
- }
- })
- .on_open({
- let context_picker = context_picker.downgrade();
- Rc::new(move |window, cx| {
- context_picker
- .update(cx, |context_picker, cx| {
- context_picker.select_first(window, cx);
- })
- .ok();
- })
- })
- .trigger_with_tooltip(
- IconButton::new("add-context", IconName::Plus)
- .icon_size(IconSize::Small)
- .style(ui::ButtonStyle::Filled),
- {
- let focus_handle = focus_handle.clone();
- move |_window, cx| {
- Tooltip::for_action_in(
- "Add Context",
- &ToggleContextPicker,
- &focus_handle,
- cx,
- )
- }
- },
- )
- .attach(gpui::Corner::TopLeft)
- .anchor(gpui::Corner::BottomLeft)
- .offset(gpui::Point {
- x: px(0.0),
- y: px(-2.0),
- })
- .with_handle(self.context_picker_menu_handle.clone()),
- )
- .children(
- added_contexts
- .into_iter()
- .enumerate()
- .map(|(i, added_context)| {
- let name = added_context.name.clone();
- let context = added_context.handle.clone();
- ContextPill::added(
- added_context,
- dupe_names.contains(&name),
- self.focused_index == Some(i),
- Some({
- let context = context.clone();
- let context_store = self.context_store.clone();
- Rc::new(cx.listener(move |_this, _event, _window, cx| {
- context_store.update(cx, |this, cx| {
- this.remove_context(&context, cx);
- });
- cx.notify();
- }))
- }),
- )
- .on_click({
- Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
- if event.click_count() > 1 {
- this.open_context(&context, window, cx);
- } else {
- this.focused_index = Some(i);
- }
- cx.notify();
- }))
- })
- }),
- )
- .when_some(suggested_context, |el, (suggested, focused)| {
- el.child(
- ContextPill::suggested(
- suggested.name().clone(),
- suggested.icon_path(),
- suggested.kind(),
- focused,
- )
- .on_click(Rc::new(cx.listener(
- move |this, _event, _window, cx| {
- this.add_suggested_context(&suggested, cx);
- },
- ))),
- )
- })
- .when(!no_added_context, {
- move |parent| {
- parent.child(
- IconButton::new("remove-all-context", IconName::Eraser)
- .icon_size(IconSize::Small)
- .tooltip({
- let focus_handle = focus_handle.clone();
- move |_window, cx| {
- Tooltip::for_action_in(
- "Remove All Context",
- &RemoveAllContext,
- &focus_handle,
- cx,
- )
- }
- })
- .on_click(cx.listener({
- let focus_handle = focus_handle.clone();
- move |_this, _event, window, cx| {
- focus_handle.dispatch_action(&RemoveAllContext, window, cx);
- }
- })),
- )
- }
- })
- .into_any()
- }
-}
-
-pub enum ContextStripEvent {
- PickerDismissed,
- BlurredEmpty,
- BlurredDown,
- BlurredUp,
-}
-
-impl EventEmitter<ContextStripEvent> for ContextStrip {}
-
-pub enum SuggestContextKind {
- Thread,
-}
-
-fn open_editor_at_position(
- project_path: project::ProjectPath,
- target_position: Point,
- workspace: &Entity<Workspace>,
- window: &mut Window,
- cx: &mut App,
-) -> Task<()> {
- let open_task = workspace.update(cx, |workspace, cx| {
- workspace.open_path(project_path, None, true, window, cx)
- });
- window.spawn(cx, async move |cx| {
- if let Some(active_editor) = open_task
- .await
- .log_err()
- .and_then(|item| item.downcast::<Editor>())
- {
- active_editor
- .downgrade()
- .update_in(cx, |editor, window, cx| {
- editor.go_to_singleton_buffer_point(target_position, window, cx);
- })
- .log_err();
- }
- })
-}
@@ -4,10 +4,11 @@ use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
+use crate::context::load_context;
+use crate::mention_set::MentionSet;
use crate::{
AgentPanel,
buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent},
- context_store::ContextStore,
inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent},
terminal_inline_assistant::TerminalInlineAssistant,
};
@@ -31,6 +32,7 @@ use editor::{
},
};
use fs::Fs;
+use futures::FutureExt;
use gpui::{
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
WeakEntity, Window, point,
@@ -214,16 +216,10 @@ impl InlineAssistant {
if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
if is_ai_enabled {
- let panel = workspace.read(cx).panel::<AgentPanel>(cx);
- let thread_store = panel
- .as_ref()
- .map(|agent_panel| agent_panel.read(cx).thread_store().downgrade());
-
editor.add_code_action_provider(
Rc::new(AssistantCodeActionProvider {
editor: cx.entity().downgrade(),
workspace: workspace.downgrade(),
- thread_store,
}),
window,
cx,
@@ -235,9 +231,6 @@ impl InlineAssistant {
editor.cancel(&Default::default(), window, cx);
}
}
-
- // Remove the Assistant1 code action provider, as it still might be registered.
- editor.remove_code_action_provider("assistant".into(), window, cx);
} else {
editor.remove_code_action_provider(
ASSISTANT_CODE_ACTION_PROVIDER_ID.into(),
@@ -279,8 +272,7 @@ impl InlineAssistant {
let agent_panel = agent_panel.read(cx);
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
- let thread_store = Some(agent_panel.thread_store().downgrade());
- let context_store = agent_panel.inline_assist_context_store().clone();
+ let thread_store = agent_panel.thread_store().clone();
let handle_assist =
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -289,10 +281,9 @@ impl InlineAssistant {
assistant.assist(
&active_editor,
cx.entity().downgrade(),
- context_store,
workspace.project().downgrade(),
- prompt_store,
thread_store,
+ prompt_store,
action.prompt.clone(),
window,
cx,
@@ -305,8 +296,8 @@ impl InlineAssistant {
&active_terminal,
cx.entity().downgrade(),
workspace.project().downgrade(),
- prompt_store,
thread_store,
+ prompt_store,
action.prompt.clone(),
window,
cx,
@@ -477,10 +468,9 @@ impl InlineAssistant {
&mut self,
editor: &Entity<Editor>,
workspace: WeakEntity<Workspace>,
- context_store: Entity<ContextStore>,
project: WeakEntity<Project>,
+ thread_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
- thread_store: Option<WeakEntity<HistoryStore>>,
initial_prompt: Option<String>,
window: &mut Window,
codegen_ranges: &[Range<Anchor>],
@@ -508,9 +498,6 @@ impl InlineAssistant {
editor.read(cx).buffer().clone(),
range.clone(),
initial_transaction_id,
- context_store.clone(),
- project.clone(),
- prompt_store.clone(),
self.telemetry.clone(),
self.prompt_builder.clone(),
cx,
@@ -526,10 +513,10 @@ impl InlineAssistant {
prompt_buffer.clone(),
codegen.clone(),
self.fs.clone(),
- context_store.clone(),
- workspace.clone(),
thread_store.clone(),
- prompt_store.as_ref().map(|s| s.downgrade()),
+ prompt_store.clone(),
+ project.clone(),
+ workspace.clone(),
window,
cx,
)
@@ -606,10 +593,9 @@ impl InlineAssistant {
&mut self,
editor: &Entity<Editor>,
workspace: WeakEntity<Workspace>,
- context_store: Entity<ContextStore>,
project: WeakEntity<Project>,
+ thread_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
- thread_store: Option<WeakEntity<HistoryStore>>,
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut App,
@@ -625,10 +611,9 @@ impl InlineAssistant {
let assist_to_focus = self.batch_assist(
editor,
workspace,
- context_store,
project,
- prompt_store,
thread_store,
+ prompt_store,
initial_prompt,
window,
&codegen_ranges,
@@ -650,8 +635,8 @@ impl InlineAssistant {
initial_transaction_id: Option<TransactionId>,
focus: bool,
workspace: Entity<Workspace>,
+ thread_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
- thread_store: Option<WeakEntity<HistoryStore>>,
window: &mut Window,
cx: &mut App,
) -> InlineAssistId {
@@ -663,16 +648,14 @@ impl InlineAssistant {
}
let project = workspace.read(cx).project().downgrade();
- let context_store = cx.new(|_cx| ContextStore::new(project.clone()));
let assist_id = self
.batch_assist(
editor,
workspace.downgrade(),
- context_store,
project,
- prompt_store,
thread_store,
+ prompt_store,
Some(initial_prompt),
window,
&[range],
@@ -1294,7 +1277,8 @@ impl InlineAssistant {
return;
}
- let Some(user_prompt) = assist.user_prompt(cx) else {
+ let Some((user_prompt, mention_set)) = assist.user_prompt(cx).zip(assist.mention_set(cx))
+ else {
return;
};
@@ -1310,9 +1294,12 @@ impl InlineAssistant {
return;
};
+ let context_task = load_context(&mention_set, cx).shared();
assist
.codegen
- .update(cx, |codegen, cx| codegen.start(model, user_prompt, cx))
+ .update(cx, |codegen, cx| {
+ codegen.start(model, user_prompt, context_task, cx)
+ })
.log_err();
}
@@ -1778,6 +1765,11 @@ impl InlineAssist {
let decorations = self.decorations.as_ref()?;
Some(decorations.prompt_editor.read(cx).prompt(cx))
}
+
+ fn mention_set(&self, cx: &App) -> Option<Entity<MentionSet>> {
+ let decorations = self.decorations.as_ref()?;
+ Some(decorations.prompt_editor.read(cx).mention_set().clone())
+ }
}
struct InlineAssistDecorations {
@@ -1790,10 +1782,9 @@ struct InlineAssistDecorations {
struct AssistantCodeActionProvider {
editor: WeakEntity<Editor>,
workspace: WeakEntity<Workspace>,
- thread_store: Option<WeakEntity<HistoryStore>>,
}
-const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2";
+const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant";
impl CodeActionProvider for AssistantCodeActionProvider {
fn id(&self) -> Arc<str> {
@@ -1861,10 +1852,20 @@ impl CodeActionProvider for AssistantCodeActionProvider {
) -> Task<Result<ProjectTransaction>> {
let editor = self.editor.clone();
let workspace = self.workspace.clone();
- let thread_store = self.thread_store.clone();
let prompt_store = PromptStore::global(cx);
window.spawn(cx, async move |cx| {
let workspace = workspace.upgrade().context("workspace was released")?;
+ let thread_store = cx.update(|_window, cx| {
+ anyhow::Ok(
+ workspace
+ .read(cx)
+ .panel::<AgentPanel>(cx)
+ .context("missing agent panel")?
+ .read(cx)
+ .thread_store()
+ .clone(),
+ )
+ })??;
let editor = editor.upgrade().context("editor was released")?;
let range = editor
.update(cx, |editor, cx| {
@@ -1907,8 +1908,8 @@ impl CodeActionProvider for AssistantCodeActionProvider {
None,
true,
workspace,
- prompt_store,
thread_store,
+ prompt_store,
window,
cx,
);
@@ -1,19 +1,21 @@
use agent::HistoryStore;
-use collections::{HashMap, VecDeque};
+use collections::VecDeque;
use editor::actions::Paste;
-use editor::display_map::{CreaseId, EditorMargins};
-use editor::{Addon, AnchorRangeExt as _, MultiBufferOffset};
+use editor::code_context_menus::CodeContextMenu;
+use editor::display_map::EditorMargins;
+use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _};
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
actions::{MoveDown, MoveUp},
};
use fs::Fs;
use gpui::{
- AnyElement, App, ClipboardEntry, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
- Focusable, Subscription, TextStyle, WeakEntity, Window,
+ AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable,
+ Subscription, TextStyle, WeakEntity, Window,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use parking_lot::Mutex;
+use project::Project;
use prompt_store::PromptStore;
use settings::Settings;
use std::cmp;
@@ -28,22 +30,21 @@ use zed_actions::agent::ToggleModelSelector;
use crate::agent_model_selector::AgentModelSelector;
use crate::buffer_codegen::BufferCodegen;
-use crate::context::{AgentContextHandle, AgentContextKey};
-use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
-use crate::context_store::{ContextStore, ContextStoreEvent};
-use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
-use crate::terminal_codegen::TerminalCodegen;
-use crate::{
- CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext, RemoveAllContext,
- ToggleContextPicker,
+use crate::completion_provider::{
+ PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,
};
+use crate::mention_set::paste_images_as_context;
+use crate::mention_set::{MentionSet, crease_for_mention};
+use crate::terminal_codegen::TerminalCodegen;
+use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
pub struct PromptEditor<T> {
pub editor: Entity<Editor>,
mode: PromptEditorMode,
- context_store: Entity<ContextStore>,
- context_strip: Entity<ContextStrip>,
- context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
+ mention_set: Entity<MentionSet>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ workspace: WeakEntity<Workspace>,
model_selector: Entity<AgentModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
@@ -51,7 +52,6 @@ pub struct PromptEditor<T> {
pending_prompt: String,
_codegen_subscription: Subscription,
editor_subscriptions: Vec<Subscription>,
- _context_strip_subscription: Subscription,
show_rate_limit_notice: bool,
_phantom: std::marker::PhantomData<T>,
}
@@ -98,6 +98,19 @@ impl<T: 'static> Render for PromptEditor<T> {
buttons.extend(self.render_buttons(window, cx));
+ let menu_visible = self.is_completions_menu_visible(cx);
+ let add_context_button = IconButton::new("add-context", IconName::AtSign)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .when(!menu_visible, |this| {
+ this.tooltip(move |_window, cx| {
+ Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
+ })
+ })
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.trigger_completion_menu(window, cx);
+ }));
+
v_flex()
.key_context("PromptEditor")
.capture_action(cx.listener(Self::paste))
@@ -114,7 +127,6 @@ impl<T: 'static> Render for PromptEditor<T> {
h_flex()
.items_start()
.cursor(CursorStyle::Arrow)
- .on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
@@ -123,7 +135,6 @@ impl<T: 'static> Render for PromptEditor<T> {
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
- .on_action(cx.listener(Self::remove_all_context))
.capture_action(cx.listener(Self::cycle_prev))
.capture_action(cx.listener(Self::cycle_next))
.child(
@@ -182,7 +193,7 @@ impl<T: 'static> Render for PromptEditor<T> {
.pl_1()
.items_start()
.justify_between()
- .child(self.context_strip.clone())
+ .child(add_context_button)
.child(self.model_selector.clone()),
),
)
@@ -214,6 +225,18 @@ impl<T: 'static> PromptEditor<T> {
));
}
+ fn assign_completion_provider(&mut self, cx: &mut Context<Self>) {
+ self.editor.update(cx, |editor, _cx| {
+ editor.set_completion_provider(Some(Rc::new(PromptCompletionProvider::new(
+ PromptEditorCompletionProviderDelegate,
+ self.mention_set.clone(),
+ self.history_store.clone(),
+ self.prompt_store.clone(),
+ self.workspace.clone(),
+ ))));
+ });
+ }
+
pub fn set_show_cursor_when_unfocused(
&mut self,
show_cursor_when_unfocused: bool,
@@ -226,27 +249,23 @@ impl<T: 'static> PromptEditor<T> {
pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let prompt = self.prompt(cx);
- let existing_creases = self.editor.update(cx, extract_message_creases);
-
+ let existing_creases = self.editor.update(cx, |editor, cx| {
+ extract_message_creases(editor, &self.mention_set, window, cx)
+ });
let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
self.editor = cx.new(|cx| {
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text("Add a promptβ¦", window, cx);
editor.set_text(prompt, window, cx);
- insert_message_creases(
- &mut editor,
- &existing_creases,
- &self.context_store,
- window,
- cx,
- );
+ insert_message_creases(&mut editor, &existing_creases, window, cx);
if focus {
window.focus(&editor.focus_handle(cx));
}
editor
});
+ self.assign_completion_provider(cx);
self.subscribe_to_editor(window, cx);
}
@@ -274,32 +293,13 @@ impl<T: 'static> PromptEditor<T> {
self.editor.read(cx).text(cx)
}
- fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
- let images = cx
- .read_from_clipboard()
- .map(|item| {
- item.into_entries()
- .filter_map(|entry| {
- if let ClipboardEntry::Image(image) = entry {
- Some(image)
- } else {
- None
- }
- })
- .collect::<Vec<_>>()
- })
- .unwrap_or_default();
-
- if images.is_empty() {
- return;
+ fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
+ if inline_assistant_model_supports_images(cx)
+ && let Some(task) =
+ paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
+ {
+ task.detach();
}
- cx.stop_propagation();
-
- self.context_store.update(cx, |store, cx| {
- for image in images {
- store.add_image_instance(Arc::new(image), cx);
- }
- });
}
fn handle_prompt_editor_events(
@@ -343,23 +343,44 @@ impl<T: 'static> PromptEditor<T> {
}
}
- fn toggle_context_picker(
- &mut self,
- _: &ToggleContextPicker,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.context_picker_menu_handle.toggle(window, cx);
+ pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
+ self.editor
+ .read(cx)
+ .context_menu()
+ .borrow()
+ .as_ref()
+ .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
}
- pub fn remove_all_context(
- &mut self,
- _: &RemoveAllContext,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.context_store.update(cx, |store, cx| store.clear(cx));
- cx.notify();
+ pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.editor.update(cx, |editor, cx| {
+ let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| {
+ matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
+ });
+
+ let has_at_sign = {
+ let snapshot = editor.display_snapshot(cx);
+ let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
+ let offset = cursor.to_offset(&snapshot);
+ if offset.0 > 0 {
+ snapshot
+ .buffer_snapshot()
+ .reversed_chars_at(offset)
+ .next()
+ .map(|sign| sign == '@')
+ .unwrap_or(false)
+ } else {
+ false
+ }
+ };
+
+ if menu_is_open && has_at_sign {
+ return;
+ }
+
+ editor.insert("@", window, cx);
+ editor.show_completions(&editor::actions::ShowCompletions, window, cx);
+ });
}
fn cancel(
@@ -434,8 +455,6 @@ impl<T: 'static> PromptEditor<T> {
editor.move_to_end(&Default::default(), window, cx)
});
}
- } else if self.context_strip.read(cx).has_context_items(cx) {
- self.context_strip.focus_handle(cx).focus(window);
}
}
@@ -709,6 +728,7 @@ impl<T: 'static> PromptEditor<T> {
EditorStyle {
background: colors.editor_background,
local_player: cx.theme().players().local(),
+ syntax: cx.theme().syntax().clone(),
text: text_style,
..Default::default()
},
@@ -716,21 +736,6 @@ impl<T: 'static> PromptEditor<T> {
})
.into_any_element()
}
-
- fn handle_context_strip_event(
- &mut self,
- _context_strip: &Entity<ContextStrip>,
- event: &ContextStripEvent,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- match event {
- ContextStripEvent::PickerDismissed
- | ContextStripEvent::BlurredEmpty
- | ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
- ContextStripEvent::BlurredDown => {}
- }
- }
}
pub enum PromptEditorMode {
@@ -765,6 +770,36 @@ impl InlineAssistId {
}
}
+struct PromptEditorCompletionProviderDelegate;
+
+fn inline_assistant_model_supports_images(cx: &App) -> bool {
+ LanguageModelRegistry::read_global(cx)
+ .inline_assistant_model()
+ .map_or(false, |m| m.model.supports_images())
+}
+
+impl PromptCompletionProviderDelegate for PromptEditorCompletionProviderDelegate {
+ fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
+ vec![
+ PromptContextType::File,
+ PromptContextType::Symbol,
+ PromptContextType::Thread,
+ PromptContextType::Fetch,
+ PromptContextType::Rules,
+ ]
+ }
+
+ fn supports_images(&self, cx: &App) -> bool {
+ inline_assistant_model_supports_images(cx)
+ }
+
+ fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
+ Vec::new()
+ }
+
+ fn confirm_command(&self, _cx: &mut App) {}
+}
+
impl PromptEditor<BufferCodegen> {
pub fn new_buffer(
id: InlineAssistId,
@@ -773,15 +808,14 @@ impl PromptEditor<BufferCodegen> {
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<BufferCodegen>,
fs: Arc<dyn Fs>,
- context_store: Entity<ContextStore>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
- thread_store: Option<WeakEntity<HistoryStore>>,
- prompt_store: Option<WeakEntity<PromptStore>>,
window: &mut Window,
cx: &mut Context<PromptEditor<BufferCodegen>>,
) -> PromptEditor<BufferCodegen> {
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
- let codegen_buffer = codegen.read(cx).buffer(cx).read(cx).as_singleton();
let mode = PromptEditorMode::Buffer {
id,
codegen,
@@ -805,7 +839,6 @@ impl PromptEditor<BufferCodegen> {
// typing in one will make what you typed appear in all of them.
editor.set_show_cursor_when_unfocused(true, cx);
editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
- editor.register_addon(ContextCreasesAddon::new());
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,
@@ -815,43 +848,25 @@ impl PromptEditor<BufferCodegen> {
editor
});
- let prompt_editor_entity = prompt_editor.downgrade();
- prompt_editor.update(cx, |editor, _| {
- editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
- workspace.clone(),
- context_store.downgrade(),
- thread_store.clone(),
+ let mention_set = cx.new(|cx| {
+ MentionSet::new(
+ prompt_editor.clone(),
+ project,
+ history_store.clone(),
prompt_store.clone(),
- prompt_editor_entity,
- codegen_buffer.as_ref().map(Entity::downgrade),
- ))));
- });
-
- let context_picker_menu_handle = PopoverMenuHandle::default();
- let model_selector_menu_handle = PopoverMenuHandle::default();
-
- let context_strip = cx.new(|cx| {
- ContextStrip::new(
- context_store.clone(),
- workspace.clone(),
- thread_store.clone(),
- prompt_store,
- context_picker_menu_handle.clone(),
- SuggestContextKind::Thread,
- ModelUsageContext::InlineAssistant,
window,
cx,
)
});
- let context_strip_subscription =
- cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
+ let model_selector_menu_handle = PopoverMenuHandle::default();
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
editor: prompt_editor.clone(),
- context_store,
- context_strip,
- context_picker_menu_handle,
+ mention_set,
+ history_store,
+ prompt_store,
+ workspace,
model_selector: cx.new(|cx| {
AgentModelSelector::new(
fs,
@@ -868,12 +883,12 @@ impl PromptEditor<BufferCodegen> {
pending_prompt: String::new(),
_codegen_subscription: codegen_subscription,
editor_subscriptions: Vec::new(),
- _context_strip_subscription: context_strip_subscription,
show_rate_limit_notice: false,
mode,
_phantom: Default::default(),
};
+ this.assign_completion_provider(cx);
this.subscribe_to_editor(window, cx);
this
}
@@ -919,6 +934,10 @@ impl PromptEditor<BufferCodegen> {
}
}
+ pub fn mention_set(&self) -> &Entity<MentionSet> {
+ &self.mention_set
+ }
+
pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
match &self.mode {
PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
@@ -945,10 +964,10 @@ impl PromptEditor<TerminalCodegen> {
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<TerminalCodegen>,
fs: Arc<dyn Fs>,
- context_store: Entity<ContextStore>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
- thread_store: Option<WeakEntity<HistoryStore>>,
- prompt_store: Option<WeakEntity<PromptStore>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -980,43 +999,25 @@ impl PromptEditor<TerminalCodegen> {
editor
});
- let prompt_editor_entity = prompt_editor.downgrade();
- prompt_editor.update(cx, |editor, _| {
- editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
- workspace.clone(),
- context_store.downgrade(),
- thread_store.clone(),
+ let mention_set = cx.new(|cx| {
+ MentionSet::new(
+ prompt_editor.clone(),
+ project,
+ history_store.clone(),
prompt_store.clone(),
- prompt_editor_entity,
- None,
- ))));
- });
-
- let context_picker_menu_handle = PopoverMenuHandle::default();
- let model_selector_menu_handle = PopoverMenuHandle::default();
-
- let context_strip = cx.new(|cx| {
- ContextStrip::new(
- context_store.clone(),
- workspace.clone(),
- thread_store.clone(),
- prompt_store.clone(),
- context_picker_menu_handle.clone(),
- SuggestContextKind::Thread,
- ModelUsageContext::InlineAssistant,
window,
cx,
)
});
- let context_strip_subscription =
- cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
+ let model_selector_menu_handle = PopoverMenuHandle::default();
let mut this = Self {
editor: prompt_editor.clone(),
- context_store,
- context_strip,
- context_picker_menu_handle,
+ mention_set,
+ history_store,
+ prompt_store,
+ workspace,
model_selector: cx.new(|cx| {
AgentModelSelector::new(
fs,
@@ -1033,12 +1034,12 @@ impl PromptEditor<TerminalCodegen> {
pending_prompt: String::new(),
_codegen_subscription: codegen_subscription,
editor_subscriptions: Vec::new(),
- _context_strip_subscription: context_strip_subscription,
mode,
show_rate_limit_notice: false,
_phantom: Default::default(),
};
this.count_lines(cx);
+ this.assign_completion_provider(cx);
this.subscribe_to_editor(window, cx);
this
}
@@ -1085,6 +1086,10 @@ impl PromptEditor<TerminalCodegen> {
}
}
+ pub fn mention_set(&self) -> &Entity<MentionSet> {
+ &self.mention_set
+ }
+
pub fn codegen(&self) -> &Entity<TerminalCodegen> {
match &self.mode {
PromptEditorMode::Buffer { .. } => unreachable!(),
@@ -1164,128 +1169,38 @@ impl GenerationMode {
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
#[derive(Clone, Debug)]
-pub struct MessageCrease {
- pub range: Range<MultiBufferOffset>,
- pub icon_path: SharedString,
- pub label: SharedString,
- /// None for a deserialized message, Some otherwise.
- pub context: Option<AgentContextHandle>,
-}
-
-#[derive(Default)]
-pub struct ContextCreasesAddon {
- creases: HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>>,
- _subscription: Option<Subscription>,
+struct MessageCrease {
+ range: Range<MultiBufferOffset>,
+ icon_path: SharedString,
+ label: SharedString,
}
-impl Addon for ContextCreasesAddon {
- fn to_any(&self) -> &dyn std::any::Any {
- self
- }
-
- fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
- Some(self)
- }
-}
-
-impl ContextCreasesAddon {
- pub fn new() -> Self {
- Self {
- creases: HashMap::default(),
- _subscription: None,
- }
- }
-
- pub fn add_creases(
- &mut self,
- context_store: &Entity<ContextStore>,
- key: AgentContextKey,
- creases: impl IntoIterator<Item = (CreaseId, SharedString)>,
- cx: &mut Context<Editor>,
- ) {
- self.creases.entry(key).or_default().extend(creases);
- self._subscription = Some(
- cx.subscribe(context_store, |editor, _, event, cx| match event {
- ContextStoreEvent::ContextRemoved(key) => {
- let Some(this) = editor.addon_mut::<Self>() else {
- return;
- };
- let (crease_ids, replacement_texts): (Vec<_>, Vec<_>) = this
- .creases
- .remove(key)
- .unwrap_or_default()
- .into_iter()
- .unzip();
- let ranges = editor
- .remove_creases(crease_ids, cx)
- .into_iter()
- .map(|(_, range)| range)
- .collect::<Vec<_>>();
- editor.unfold_ranges(&ranges, false, false, cx);
- editor.edit(ranges.into_iter().zip(replacement_texts), cx);
- cx.notify();
- }
- }),
- )
- }
-
- pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
- self.creases
- }
-}
-
-pub fn extract_message_creases(
+fn extract_message_creases(
editor: &mut Editor,
+ mention_set: &Entity<MentionSet>,
+ window: &mut Window,
cx: &mut Context<'_, Editor>,
) -> Vec<MessageCrease> {
- let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
- let mut contexts_by_crease_id = editor
- .addon_mut::<ContextCreasesAddon>()
- .map(std::mem::take)
- .unwrap_or_default()
- .into_inner()
- .into_iter()
- .flat_map(|(key, creases)| {
- let context = key.0;
- creases
- .into_iter()
- .map(move |(id, _)| (id, context.clone()))
- })
- .collect::<HashMap<_, _>>();
- // Filter the addon's list of creases based on what the editor reports,
- // since the addon might have removed creases in it.
-
- editor.display_map.update(cx, |display_map, cx| {
- display_map
- .snapshot(cx)
- .crease_snapshot
- .creases()
- .filter_map(|(id, crease)| {
- Some((
- id,
- (
- crease.range().to_offset(&buffer_snapshot),
- crease.metadata()?.clone(),
- ),
- ))
+ let creases = mention_set.read(cx).creases();
+ let snapshot = editor.snapshot(window, cx);
+ snapshot
+ .crease_snapshot
+ .creases()
+ .filter(|(id, _)| creases.contains(id))
+ .filter_map(|(_, crease)| {
+ let metadata = crease.metadata()?.clone();
+ Some(MessageCrease {
+ range: crease.range().to_offset(snapshot.buffer()),
+ label: metadata.label,
+ icon_path: metadata.icon_path,
})
- .map(|(id, (range, metadata))| {
- let context = contexts_by_crease_id.remove(&id);
- MessageCrease {
- range,
- context,
- label: metadata.label,
- icon_path: metadata.icon_path,
- }
- })
- .collect()
- })
+ })
+ .collect()
}
-pub fn insert_message_creases(
+fn insert_message_creases(
editor: &mut Editor,
message_creases: &[MessageCrease],
- context_store: &Entity<ContextStore>,
window: &mut Window,
cx: &mut Context<'_, Editor>,
) {
@@ -1303,14 +1218,6 @@ pub fn insert_message_creases(
)
})
.collect::<Vec<_>>();
- let ids = editor.insert_creases(creases.clone(), cx);
+ editor.insert_creases(creases.clone(), cx);
editor.fold_creases(creases, false, window, cx);
- if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
- for (crease, id) in message_creases.iter().zip(ids) {
- if let Some(context) = crease.context.as_ref() {
- let key = AgentContextKey(context.clone());
- addon.add_creases(context_store, key, vec![(id, crease.label.clone())], cx);
- }
- }
- }
}
@@ -0,0 +1,1112 @@
+use acp_thread::{MentionUri, selection_name};
+use agent::{HistoryStore, outline};
+use agent_client_protocol as acp;
+use agent_servers::{AgentServer, AgentServerDelegate};
+use anyhow::{Context as _, Result, anyhow};
+use assistant_slash_commands::codeblock_fence_for_path;
+use collections::{HashMap, HashSet};
+use editor::{
+ Anchor, Editor, EditorEvent, EditorSnapshot, ExcerptId, FoldPlaceholder, ToOffset,
+ display_map::{Crease, CreaseId, CreaseMetadata, FoldId},
+ scroll::Autoscroll,
+};
+use futures::{AsyncReadExt as _, FutureExt as _, future::Shared};
+use gpui::{
+ Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Empty, Entity, EntityId,
+ Image, ImageFormat, Img, SharedString, Subscription, Task, WeakEntity, pulsating_between,
+};
+use http_client::{AsyncBody, HttpClientWithUrl};
+use itertools::Either;
+use language::Buffer;
+use language_model::LanguageModelImage;
+use multi_buffer::MultiBufferRow;
+use postage::stream::Stream as _;
+use project::{Project, ProjectItem, ProjectPath, Worktree};
+use prompt_store::{PromptId, PromptStore};
+use rope::Point;
+use std::{
+ cell::RefCell,
+ ffi::OsStr,
+ fmt::Write,
+ ops::{Range, RangeInclusive},
+ path::{Path, PathBuf},
+ rc::Rc,
+ sync::Arc,
+ time::Duration,
+};
+use text::OffsetRangeExt;
+use ui::{ButtonLike, Disclosure, TintColor, Toggleable, prelude::*};
+use util::{ResultExt, debug_panic, rel_path::RelPath};
+use workspace::{Workspace, notifications::NotifyResultExt as _};
+
+pub type MentionTask = Shared<Task<Result<Mention, String>>>;
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum Mention {
+ Text {
+ content: String,
+ tracked_buffers: Vec<Entity<Buffer>>,
+ },
+ Image(MentionImage),
+ Link,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct MentionImage {
+ pub data: SharedString,
+ pub format: ImageFormat,
+}
+
+pub struct MentionSet {
+ editor: Entity<Editor>,
+ project: WeakEntity<Project>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,
+ _editor_subscription: Subscription,
+}
+
+impl MentionSet {
+ pub fn new(
+ editor: Entity<Editor>,
+ project: WeakEntity<Project>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let editor_subscription =
+ cx.subscribe_in(&editor, window, move |this, editor, event, window, cx| {
+ if let EditorEvent::Edited { .. } = event
+ && !editor.read(cx).read_only(cx)
+ {
+ let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
+ this.remove_invalid(snapshot);
+ }
+ });
+
+ Self {
+ editor,
+ project,
+ history_store,
+ prompt_store,
+ mentions: HashMap::default(),
+ _editor_subscription: editor_subscription,
+ }
+ }
+
+ pub fn contents(
+ &self,
+ full_mention_content: bool,
+ cx: &mut App,
+ ) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
+ let Some(project) = self.project.upgrade() else {
+ return Task::ready(Err(anyhow!("Project not found")));
+ };
+ let mentions = self.mentions.clone();
+ cx.spawn(async move |cx| {
+ let mut contents = HashMap::default();
+ for (crease_id, (mention_uri, task)) in mentions {
+ let content = if full_mention_content
+ && let MentionUri::Directory { abs_path } = &mention_uri
+ {
+ cx.update(|cx| full_mention_for_directory(&project, abs_path, cx))?
+ .await?
+ } else {
+ task.await.map_err(|e| anyhow!("{e}"))?
+ };
+
+ contents.insert(crease_id, (mention_uri, content));
+ }
+ Ok(contents)
+ })
+ }
+
+ fn remove_invalid(&mut self, snapshot: EditorSnapshot) {
+ for (crease_id, crease) in snapshot.crease_snapshot.creases() {
+ if !crease.range().start.is_valid(&snapshot.buffer_snapshot()) {
+ self.mentions.remove(&crease_id);
+ }
+ }
+ }
+
+ pub fn insert_mention(&mut self, crease_id: CreaseId, uri: MentionUri, task: MentionTask) {
+ self.mentions.insert(crease_id, (uri, task));
+ }
+
+ pub fn remove_mention(&mut self, crease_id: &CreaseId) {
+ self.mentions.remove(crease_id);
+ }
+
+ pub fn creases(&self) -> HashSet<CreaseId> {
+ self.mentions.keys().cloned().collect()
+ }
+
+ pub fn mentions(&self) -> HashSet<MentionUri> {
+ self.mentions.values().map(|(uri, _)| uri.clone()).collect()
+ }
+
+ pub fn remove_all(&mut self) -> impl Iterator<Item = (CreaseId, (MentionUri, MentionTask))> {
+ self.mentions.drain()
+ }
+
+ pub fn confirm_mention_completion(
+ &mut self,
+ crease_text: SharedString,
+ start: text::Anchor,
+ content_len: usize,
+ mention_uri: MentionUri,
+ supports_images: bool,
+ workspace: &Entity<Workspace>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Task<()> {
+ let Some(project) = self.project.upgrade() else {
+ return Task::ready(());
+ };
+
+ let snapshot = self
+ .editor
+ .update(cx, |editor, cx| editor.snapshot(window, cx));
+ let Some(start_anchor) = snapshot.buffer_snapshot().as_singleton_anchor(start) else {
+ return Task::ready(());
+ };
+ let excerpt_id = start_anchor.excerpt_id;
+ let end_anchor = snapshot.buffer_snapshot().anchor_before(
+ start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1usize,
+ );
+
+ let crease = if let MentionUri::File { abs_path } = &mention_uri
+ && let Some(extension) = abs_path.extension()
+ && let Some(extension) = extension.to_str()
+ && Img::extensions().contains(&extension)
+ && !extension.contains("svg")
+ {
+ let Some(project_path) = project
+ .read(cx)
+ .project_path_for_absolute_path(&abs_path, cx)
+ else {
+ log::error!("project path not found");
+ return Task::ready(());
+ };
+ let image_task = project.update(cx, |project, cx| project.open_image(project_path, cx));
+ let image = cx
+ .spawn(async move |_, cx| {
+ let image = image_task.await.map_err(|e| e.to_string())?;
+ let image = image
+ .update(cx, |image, _| image.image.clone())
+ .map_err(|e| e.to_string())?;
+ Ok(image)
+ })
+ .shared();
+ insert_crease_for_mention(
+ excerpt_id,
+ start,
+ content_len,
+ mention_uri.name().into(),
+ IconName::Image.path().into(),
+ Some(image),
+ self.editor.clone(),
+ window,
+ cx,
+ )
+ } else {
+ insert_crease_for_mention(
+ excerpt_id,
+ start,
+ content_len,
+ crease_text,
+ mention_uri.icon_path(cx),
+ None,
+ self.editor.clone(),
+ window,
+ cx,
+ )
+ };
+ let Some((crease_id, tx)) = crease else {
+ return Task::ready(());
+ };
+
+ let task = match mention_uri.clone() {
+ MentionUri::Fetch { url } => {
+ self.confirm_mention_for_fetch(url, workspace.read(cx).client().http_client(), cx)
+ }
+ MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
+ MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
+ MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
+ MentionUri::File { abs_path } => {
+ self.confirm_mention_for_file(abs_path, supports_images, cx)
+ }
+ MentionUri::Symbol {
+ abs_path,
+ line_range,
+ ..
+ } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
+ MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
+ MentionUri::PastedImage => {
+ debug_panic!("pasted image URI should not be included in completions");
+ Task::ready(Err(anyhow!(
+ "pasted imaged URI should not be included in completions"
+ )))
+ }
+ MentionUri::Selection { .. } => {
+ debug_panic!("unexpected selection URI");
+ Task::ready(Err(anyhow!("unexpected selection URI")))
+ }
+ };
+ let task = cx
+ .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
+ .shared();
+ self.mentions.insert(crease_id, (mention_uri, task.clone()));
+
+ // Notify the user if we failed to load the mentioned context
+ cx.spawn_in(window, async move |this, cx| {
+ let result = task.await.notify_async_err(cx);
+ drop(tx);
+ if result.is_none() {
+ this.update(cx, |this, cx| {
+ this.editor.update(cx, |editor, cx| {
+ // Remove mention
+ editor.edit([(start_anchor..end_anchor, "")], cx);
+ });
+ this.mentions.remove(&crease_id);
+ })
+ .ok();
+ }
+ })
+ }
+
+ pub fn confirm_mention_for_file(
+ &self,
+ abs_path: PathBuf,
+ supports_images: bool,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let Some(project) = self.project.upgrade() else {
+ return Task::ready(Err(anyhow!("project not found")));
+ };
+
+ let Some(project_path) = project
+ .read(cx)
+ .project_path_for_absolute_path(&abs_path, cx)
+ else {
+ return Task::ready(Err(anyhow!("project path not found")));
+ };
+ let extension = abs_path
+ .extension()
+ .and_then(OsStr::to_str)
+ .unwrap_or_default();
+
+ if Img::extensions().contains(&extension) && !extension.contains("svg") {
+ if !supports_images {
+ return Task::ready(Err(anyhow!("This model does not support images yet")));
+ }
+ let task = project.update(cx, |project, cx| project.open_image(project_path, cx));
+ return cx.spawn(async move |_, cx| {
+ let image = task.await?;
+ let image = image.update(cx, |image, _| image.image.clone())?;
+ let format = image.format;
+ let image = cx
+ .update(|cx| LanguageModelImage::from_image(image, cx))?
+ .await;
+ if let Some(image) = image {
+ Ok(Mention::Image(MentionImage {
+ data: image.source,
+ format,
+ }))
+ } else {
+ Err(anyhow!("Failed to convert image"))
+ }
+ });
+ }
+
+ let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
+ cx.spawn(async move |_, cx| {
+ let buffer = buffer.await?;
+ let buffer_content = outline::get_buffer_content_or_outline(
+ buffer.clone(),
+ Some(&abs_path.to_string_lossy()),
+ &cx,
+ )
+ .await?;
+
+ Ok(Mention::Text {
+ content: buffer_content.text,
+ tracked_buffers: vec![buffer],
+ })
+ })
+ }
+
+ fn confirm_mention_for_fetch(
+ &self,
+ url: url::Url,
+ http_client: Arc<HttpClientWithUrl>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ cx.background_executor().spawn(async move {
+ let content = fetch_url_content(http_client, url.to_string()).await?;
+ Ok(Mention::Text {
+ content,
+ tracked_buffers: Vec::new(),
+ })
+ })
+ }
+
+ fn confirm_mention_for_symbol(
+ &self,
+ abs_path: PathBuf,
+ line_range: RangeInclusive<u32>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let Some(project) = self.project.upgrade() else {
+ return Task::ready(Err(anyhow!("project not found")));
+ };
+ let Some(project_path) = project
+ .read(cx)
+ .project_path_for_absolute_path(&abs_path, cx)
+ else {
+ return Task::ready(Err(anyhow!("project path not found")));
+ };
+ let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
+ cx.spawn(async move |_, cx| {
+ let buffer = buffer.await?;
+ let mention = buffer.update(cx, |buffer, cx| {
+ let start = Point::new(*line_range.start(), 0).min(buffer.max_point());
+ let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point());
+ let content = buffer.text_for_range(start..end).collect();
+ Mention::Text {
+ content,
+ tracked_buffers: vec![cx.entity()],
+ }
+ })?;
+ anyhow::Ok(mention)
+ })
+ }
+
+ fn confirm_mention_for_rule(
+ &mut self,
+ id: PromptId,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let Some(prompt_store) = self.prompt_store.as_ref() else {
+ return Task::ready(Err(anyhow!("Missing prompt store")));
+ };
+ let prompt = prompt_store.read(cx).load(id, cx);
+ cx.spawn(async move |_, _| {
+ let prompt = prompt.await?;
+ Ok(Mention::Text {
+ content: prompt,
+ tracked_buffers: Vec::new(),
+ })
+ })
+ }
+
+ pub fn confirm_mention_for_selection(
+ &mut self,
+ source_range: Range<text::Anchor>,
+ selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(project) = self.project.upgrade() else {
+ return;
+ };
+
+ let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
+ let Some(start) = snapshot.as_singleton_anchor(source_range.start) else {
+ return;
+ };
+
+ let offset = start.to_offset(&snapshot);
+
+ for (buffer, selection_range, range_to_fold) in selections {
+ let range = snapshot.anchor_after(offset + range_to_fold.start)
+ ..snapshot.anchor_after(offset + range_to_fold.end);
+
+ let abs_path = buffer
+ .read(cx)
+ .project_path(cx)
+ .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx));
+ let snapshot = buffer.read(cx).snapshot();
+
+ let text = snapshot
+ .text_for_range(selection_range.clone())
+ .collect::<String>();
+ let point_range = selection_range.to_point(&snapshot);
+ let line_range = point_range.start.row..=point_range.end.row;
+
+ let uri = MentionUri::Selection {
+ abs_path: abs_path.clone(),
+ line_range: line_range.clone(),
+ };
+ let crease = crease_for_mention(
+ selection_name(abs_path.as_deref(), &line_range).into(),
+ uri.icon_path(cx),
+ range,
+ self.editor.downgrade(),
+ );
+
+ let crease_id = self.editor.update(cx, |editor, cx| {
+ let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
+ editor.fold_creases(vec![crease], false, window, cx);
+ crease_ids.first().copied().unwrap()
+ });
+
+ self.mentions.insert(
+ crease_id,
+ (
+ uri,
+ Task::ready(Ok(Mention::Text {
+ content: text,
+ tracked_buffers: vec![buffer],
+ }))
+ .shared(),
+ ),
+ );
+ }
+
+ // Take this explanation with a grain of salt but, with creases being
+ // inserted, GPUI's recomputes the editor layout in the next frames, so
+ // directly calling `editor.request_autoscroll` wouldn't work as
+ // expected. We're leveraging `cx.on_next_frame` to wait 2 frames and
+ // ensure that the layout has been recalculated so that the autoscroll
+ // request actually shows the cursor's new position.
+ let editor = self.editor.clone();
+ cx.on_next_frame(window, move |_, window, cx| {
+ cx.on_next_frame(window, move |_, _, cx| {
+ editor.update(cx, |editor, cx| {
+ editor.request_autoscroll(Autoscroll::fit(), cx)
+ });
+ });
+ });
+ }
+
+ fn confirm_mention_for_thread(
+ &mut self,
+ id: acp::SessionId,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let Some(project) = self.project.upgrade() else {
+ return Task::ready(Err(anyhow!("project not found")));
+ };
+
+ let server = Rc::new(agent::NativeAgentServer::new(
+ project.read(cx).fs().clone(),
+ self.history_store.clone(),
+ ));
+ let delegate = AgentServerDelegate::new(
+ project.read(cx).agent_server_store().clone(),
+ project.clone(),
+ None,
+ None,
+ );
+ let connection = server.connect(None, delegate, cx);
+ cx.spawn(async move |_, cx| {
+ let (agent, _) = connection.await?;
+ let agent = agent.downcast::<agent::NativeAgentConnection>().unwrap();
+ let summary = agent
+ .0
+ .update(cx, |agent, cx| agent.thread_summary(id, cx))?
+ .await?;
+ anyhow::Ok(Mention::Text {
+ content: summary.to_string(),
+ tracked_buffers: Vec::new(),
+ })
+ })
+ }
+
+ fn confirm_mention_for_text_thread(
+ &mut self,
+ path: PathBuf,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let text_thread_task = self.history_store.update(cx, |store, cx| {
+ store.load_text_thread(path.as_path().into(), cx)
+ });
+ cx.spawn(async move |_, cx| {
+ let text_thread = text_thread_task.await?;
+ let xml = text_thread.update(cx, |text_thread, cx| text_thread.to_xml(cx))?;
+ Ok(Mention::Text {
+ content: xml,
+ tracked_buffers: Vec::new(),
+ })
+ })
+ }
+}
+
+pub(crate) fn paste_images_as_context(
+ editor: Entity<Editor>,
+ mention_set: Entity<MentionSet>,
+ window: &mut Window,
+ cx: &mut App,
+) -> Option<Task<()>> {
+ let clipboard = cx.read_from_clipboard()?;
+ Some(window.spawn(cx, async move |cx| {
+ use itertools::Itertools;
+ let (mut images, paths) = clipboard
+ .into_entries()
+ .filter_map(|entry| match entry {
+ ClipboardEntry::Image(image) => Some(Either::Left(image)),
+ ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
+ _ => None,
+ })
+ .partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
+
+ if !paths.is_empty() {
+ images.extend(
+ cx.background_spawn(async move {
+ let mut images = vec![];
+ for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) {
+ let Ok(content) = async_fs::read(path).await else {
+ continue;
+ };
+ let Ok(format) = image::guess_format(&content) else {
+ continue;
+ };
+ images.push(gpui::Image::from_bytes(
+ match format {
+ image::ImageFormat::Png => gpui::ImageFormat::Png,
+ image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
+ image::ImageFormat::WebP => gpui::ImageFormat::Webp,
+ image::ImageFormat::Gif => gpui::ImageFormat::Gif,
+ image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
+ image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
+ image::ImageFormat::Ico => gpui::ImageFormat::Ico,
+ _ => continue,
+ },
+ content,
+ ));
+ }
+ images
+ })
+ .await,
+ );
+ }
+
+ if images.is_empty() {
+ return;
+ }
+
+ let replacement_text = MentionUri::PastedImage.as_link().to_string();
+ cx.update(|_window, cx| {
+ cx.stop_propagation();
+ })
+ .ok();
+ for image in images {
+ let Ok((excerpt_id, text_anchor, multibuffer_anchor)) =
+ editor.update_in(cx, |message_editor, window, cx| {
+ let snapshot = message_editor.snapshot(window, cx);
+ let (excerpt_id, _, buffer_snapshot) =
+ snapshot.buffer_snapshot().as_singleton().unwrap();
+
+ let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
+ let multibuffer_anchor = snapshot
+ .buffer_snapshot()
+ .anchor_in_excerpt(*excerpt_id, text_anchor);
+ message_editor.edit(
+ [(
+ multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
+ format!("{replacement_text} "),
+ )],
+ cx,
+ );
+ (*excerpt_id, text_anchor, multibuffer_anchor)
+ })
+ else {
+ break;
+ };
+
+ let content_len = replacement_text.len();
+ let Some(start_anchor) = multibuffer_anchor else {
+ continue;
+ };
+ let Ok(end_anchor) = editor.update(cx, |editor, cx| {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
+ }) else {
+ continue;
+ };
+ let image = Arc::new(image);
+ let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
+ insert_crease_for_mention(
+ excerpt_id,
+ text_anchor,
+ content_len,
+ MentionUri::PastedImage.name().into(),
+ IconName::Image.path().into(),
+ Some(Task::ready(Ok(image.clone())).shared()),
+ editor.clone(),
+ window,
+ cx,
+ )
+ }) else {
+ continue;
+ };
+ let task = cx
+ .spawn(async move |cx| {
+ let format = image.format;
+ let image = cx
+ .update(|_, cx| LanguageModelImage::from_image(image, cx))
+ .map_err(|e| e.to_string())?
+ .await;
+ drop(tx);
+ if let Some(image) = image {
+ Ok(Mention::Image(MentionImage {
+ data: image.source,
+ format,
+ }))
+ } else {
+ Err("Failed to convert image".into())
+ }
+ })
+ .shared();
+
+ mention_set
+ .update(cx, |mention_set, _cx| {
+ mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone())
+ })
+ .ok();
+
+ if task.await.notify_async_err(cx).is_none() {
+ editor
+ .update(cx, |editor, cx| {
+ editor.edit([(start_anchor..end_anchor, "")], cx);
+ })
+ .ok();
+ mention_set
+ .update(cx, |mention_set, _cx| {
+ mention_set.remove_mention(&crease_id)
+ })
+ .ok();
+ }
+ }
+ }))
+}
+
+pub(crate) fn insert_crease_for_mention(
+ excerpt_id: ExcerptId,
+ anchor: text::Anchor,
+ content_len: usize,
+ crease_label: SharedString,
+ crease_icon: SharedString,
+ // abs_path: Option<Arc<Path>>,
+ image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
+ editor: Entity<Editor>,
+ window: &mut Window,
+ cx: &mut App,
+) -> Option<(CreaseId, postage::barrier::Sender)> {
+ let (tx, rx) = postage::barrier::channel();
+
+ let crease_id = editor.update(cx, |editor, cx| {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+
+ let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
+
+ let start = start.bias_right(&snapshot);
+ let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
+
+ let placeholder = FoldPlaceholder {
+ render: render_mention_fold_button(
+ crease_label.clone(),
+ crease_icon.clone(),
+ start..end,
+ rx,
+ image,
+ cx.weak_entity(),
+ cx,
+ ),
+ merge_adjacent: false,
+ ..Default::default()
+ };
+
+ let crease = Crease::Inline {
+ range: start..end,
+ placeholder,
+ render_toggle: None,
+ render_trailer: None,
+ metadata: Some(CreaseMetadata {
+ label: crease_label,
+ icon_path: crease_icon,
+ }),
+ };
+
+ let ids = editor.insert_creases(vec![crease.clone()], cx);
+ editor.fold_creases(vec![crease], false, window, cx);
+
+ Some(ids[0])
+ })?;
+
+ Some((crease_id, tx))
+}
+
+pub(crate) fn crease_for_mention(
+ label: SharedString,
+ icon_path: SharedString,
+ range: Range<Anchor>,
+ editor_entity: WeakEntity<Editor>,
+) -> Crease<Anchor> {
+ let placeholder = FoldPlaceholder {
+ render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
+ merge_adjacent: false,
+ ..Default::default()
+ };
+
+ let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
+
+ Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
+ .with_metadata(CreaseMetadata { icon_path, label })
+}
+
+fn render_fold_icon_button(
+ icon_path: SharedString,
+ label: SharedString,
+ editor: WeakEntity<Editor>,
+) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
+ Arc::new({
+ move |fold_id, fold_range, cx| {
+ let is_in_text_selection = editor
+ .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
+ .unwrap_or_default();
+
+ ButtonLike::new(fold_id)
+ .style(ButtonStyle::Filled)
+ .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+ .toggle_state(is_in_text_selection)
+ .child(
+ h_flex()
+ .gap_1()
+ .child(
+ Icon::from_path(icon_path.clone())
+ .size(IconSize::XSmall)
+ .color(Color::Muted),
+ )
+ .child(
+ Label::new(label.clone())
+ .size(LabelSize::Small)
+ .buffer_font(cx)
+ .single_line(),
+ ),
+ )
+ .into_any_element()
+ }
+ })
+}
+
+fn fold_toggle(
+ name: &'static str,
+) -> impl Fn(
+ MultiBufferRow,
+ bool,
+ Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
+ &mut Window,
+ &mut App,
+) -> AnyElement {
+ move |row, is_folded, fold, _window, _cx| {
+ Disclosure::new((name, row.0 as u64), !is_folded)
+ .toggle_state(is_folded)
+ .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
+ .into_any_element()
+ }
+}
+
+fn full_mention_for_directory(
+ project: &Entity<Project>,
+ abs_path: &Path,
+ cx: &mut App,
+) -> Task<Result<Mention>> {
+ fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
+ let mut files = Vec::new();
+
+ for entry in worktree.child_entries(path) {
+ if entry.is_dir() {
+ files.extend(collect_files_in_path(worktree, &entry.path));
+ } else if entry.is_file() {
+ files.push((
+ entry.path.clone(),
+ worktree
+ .full_path(&entry.path)
+ .to_string_lossy()
+ .to_string(),
+ ));
+ }
+ }
+
+ files
+ }
+
+ let Some(project_path) = project
+ .read(cx)
+ .project_path_for_absolute_path(&abs_path, cx)
+ else {
+ return Task::ready(Err(anyhow!("project path not found")));
+ };
+ let Some(entry) = project.read(cx).entry_for_path(&project_path, cx) else {
+ return Task::ready(Err(anyhow!("project entry not found")));
+ };
+ let directory_path = entry.path.clone();
+ let worktree_id = project_path.worktree_id;
+ let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
+ return Task::ready(Err(anyhow!("worktree not found")));
+ };
+ let project = project.clone();
+ cx.spawn(async move |cx| {
+ let file_paths = worktree.read_with(cx, |worktree, _cx| {
+ collect_files_in_path(worktree, &directory_path)
+ })?;
+ let descendants_future = cx.update(|cx| {
+ futures::future::join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
+ let rel_path = worktree_path
+ .strip_prefix(&directory_path)
+ .log_err()
+ .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
+
+ let open_task = project.update(cx, |project, cx| {
+ project.buffer_store().update(cx, |buffer_store, cx| {
+ let project_path = ProjectPath {
+ worktree_id,
+ path: worktree_path,
+ };
+ buffer_store.open_buffer(project_path, cx)
+ })
+ });
+
+ cx.spawn(async move |cx| {
+ let buffer = open_task.await.log_err()?;
+ let buffer_content = outline::get_buffer_content_or_outline(
+ buffer.clone(),
+ Some(&full_path),
+ &cx,
+ )
+ .await
+ .ok()?;
+
+ Some((rel_path, full_path, buffer_content.text, buffer))
+ })
+ }))
+ })?;
+
+ let contents = cx
+ .background_spawn(async move {
+ let (contents, tracked_buffers) = descendants_future
+ .await
+ .into_iter()
+ .flatten()
+ .map(|(rel_path, full_path, rope, buffer)| {
+ ((rel_path, full_path, rope), buffer)
+ })
+ .unzip();
+ Mention::Text {
+ content: render_directory_contents(contents),
+ tracked_buffers,
+ }
+ })
+ .await;
+ anyhow::Ok(contents)
+ })
+}
+
+fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
+ let mut output = String::new();
+ for (_relative_path, full_path, content) in entries {
+ let fence = codeblock_fence_for_path(Some(&full_path), None);
+ write!(output, "\n{fence}\n{content}\n```").unwrap();
+ }
+ output
+}
+
+fn render_mention_fold_button(
+ label: SharedString,
+ icon: SharedString,
+ range: Range<Anchor>,
+ mut loading_finished: postage::barrier::Receiver,
+ image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
+ editor: WeakEntity<Editor>,
+ cx: &mut App,
+) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
+ let loading = cx.new(|cx| {
+ let loading = cx.spawn(async move |this, cx| {
+ loading_finished.recv().await;
+ this.update(cx, |this: &mut LoadingContext, cx| {
+ this.loading = None;
+ cx.notify();
+ })
+ .ok();
+ });
+ LoadingContext {
+ id: cx.entity_id(),
+ label,
+ icon,
+ range,
+ editor,
+ loading: Some(loading),
+ image: image_task.clone(),
+ }
+ });
+ Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
+}
+
+struct LoadingContext {
+ id: EntityId,
+ label: SharedString,
+ icon: SharedString,
+ range: Range<Anchor>,
+ editor: WeakEntity<Editor>,
+ loading: Option<Task<()>>,
+ image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
+}
+
+impl Render for LoadingContext {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let is_in_text_selection = self
+ .editor
+ .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
+ .unwrap_or_default();
+ ButtonLike::new(("loading-context", self.id))
+ .style(ButtonStyle::Filled)
+ .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+ .toggle_state(is_in_text_selection)
+ .when_some(self.image.clone(), |el, image_task| {
+ el.hoverable_tooltip(move |_, cx| {
+ let image = image_task.peek().cloned().transpose().ok().flatten();
+ let image_task = image_task.clone();
+ cx.new::<ImageHover>(|cx| ImageHover {
+ image,
+ _task: cx.spawn(async move |this, cx| {
+ if let Ok(image) = image_task.clone().await {
+ this.update(cx, |this, cx| {
+ if this.image.replace(image).is_none() {
+ cx.notify();
+ }
+ })
+ .ok();
+ }
+ }),
+ })
+ .into()
+ })
+ })
+ .child(
+ h_flex()
+ .gap_1()
+ .child(
+ Icon::from_path(self.icon.clone())
+ .size(IconSize::XSmall)
+ .color(Color::Muted),
+ )
+ .child(
+ Label::new(self.label.clone())
+ .size(LabelSize::Small)
+ .buffer_font(cx)
+ .single_line(),
+ )
+ .map(|el| {
+ if self.loading.is_some() {
+ el.with_animation(
+ "loading-context-crease",
+ Animation::new(Duration::from_secs(2))
+ .repeat()
+ .with_easing(pulsating_between(0.4, 0.8)),
+ |label, delta| label.opacity(delta),
+ )
+ .into_any()
+ } else {
+ el.into_any()
+ }
+ }),
+ )
+ }
+}
+
+struct ImageHover {
+ image: Option<Arc<Image>>,
+ _task: Task<()>,
+}
+
+impl Render for ImageHover {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ if let Some(image) = self.image.clone() {
+ gpui::img(image).max_w_96().max_h_96().into_any_element()
+ } else {
+ gpui::Empty.into_any_element()
+ }
+ }
+}
+
+async fn fetch_url_content(http_client: Arc<HttpClientWithUrl>, url: String) -> Result<String> {
+ #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+ enum ContentType {
+ Html,
+ Plaintext,
+ Json,
+ }
+ use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
+
+ let url = if !url.starts_with("https://") && !url.starts_with("http://") {
+ format!("https://{url}")
+ } else {
+ url
+ };
+
+ let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
+ let mut body = Vec::new();
+ response
+ .body_mut()
+ .read_to_end(&mut body)
+ .await
+ .context("error reading response body")?;
+
+ if response.status().is_client_error() {
+ let text = String::from_utf8_lossy(body.as_slice());
+ anyhow::bail!(
+ "status error {}, response: {text:?}",
+ response.status().as_u16()
+ );
+ }
+
+ let Some(content_type) = response.headers().get("content-type") else {
+ anyhow::bail!("missing Content-Type header");
+ };
+ let content_type = content_type
+ .to_str()
+ .context("invalid Content-Type header")?;
+ let content_type = match content_type {
+ "text/html" => ContentType::Html,
+ "text/plain" => ContentType::Plaintext,
+ "application/json" => ContentType::Json,
+ _ => ContentType::Html,
+ };
+
+ match content_type {
+ ContentType::Html => {
+ let mut handlers: Vec<TagHandler> = vec![
+ Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
+ Rc::new(RefCell::new(markdown::ParagraphHandler)),
+ Rc::new(RefCell::new(markdown::HeadingHandler)),
+ Rc::new(RefCell::new(markdown::ListHandler)),
+ Rc::new(RefCell::new(markdown::TableHandler::new())),
+ Rc::new(RefCell::new(markdown::StyledTextHandler)),
+ ];
+ if url.contains("wikipedia.org") {
+ use html_to_markdown::structure::wikipedia;
+
+ handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
+ handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
+ handlers.push(Rc::new(
+ RefCell::new(wikipedia::WikipediaCodeHandler::new()),
+ ));
+ } else {
+ handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
+ }
+ convert_html_to_markdown(&body[..], &mut handlers)
+ }
+ ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
+ ContentType::Json => {
+ let json: serde_json::Value = serde_json::from_slice(&body)?;
+
+ Ok(format!(
+ "```json\n{}\n```",
+ serde_json::to_string_pretty(&json)?
+ ))
+ }
+ }
+}
@@ -1,6 +1,5 @@
use crate::{
context::load_context,
- context_store::ContextStore,
inline_prompt_editor::{
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
},
@@ -73,8 +72,8 @@ impl TerminalInlineAssistant {
terminal_view: &Entity<TerminalView>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
+ thread_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
- thread_store: Option<WeakEntity<HistoryStore>>,
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut App,
@@ -87,7 +86,6 @@ impl TerminalInlineAssistant {
cx,
)
});
- let context_store = cx.new(|_cx| ContextStore::new(project));
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new(|cx| {
@@ -97,10 +95,10 @@ impl TerminalInlineAssistant {
prompt_buffer.clone(),
codegen,
self.fs.clone(),
- context_store.clone(),
- workspace.clone(),
thread_store.clone(),
- prompt_store.as_ref().map(|s| s.downgrade()),
+ prompt_store.clone(),
+ project.clone(),
+ workspace.clone(),
window,
cx,
)
@@ -119,8 +117,6 @@ impl TerminalInlineAssistant {
terminal_view,
prompt_editor,
workspace.clone(),
- context_store,
- prompt_store,
window,
cx,
);
@@ -227,6 +223,10 @@ impl TerminalInlineAssistant {
assist_id: TerminalInlineAssistId,
cx: &mut App,
) -> Result<Task<LanguageModelRequest>> {
+ let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
+ .inline_assistant_model()
+ .context("No inline assistant model")?;
+
let assist = self.assists.get(&assist_id).context("invalid assist")?;
let shell = std::env::var("SHELL").ok();
@@ -243,35 +243,20 @@ impl TerminalInlineAssistant {
.ok()
.unwrap_or_default();
+ let prompt_editor = assist.prompt_editor.clone().context("invalid assist")?;
+
let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
- &assist
- .prompt_editor
- .clone()
- .context("invalid assist")?
- .read(cx)
- .prompt(cx),
+ &prompt_editor.read(cx).prompt(cx),
shell.as_deref(),
working_directory.as_deref(),
&latest_output,
)?;
- let contexts = assist
- .context_store
- .read(cx)
- .context()
- .cloned()
- .collect::<Vec<_>>();
- let context_load_task = assist.workspace.update(cx, |workspace, cx| {
- let project = workspace.project();
- load_context(contexts, project, &assist.prompt_store, cx)
- })?;
-
- let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
- .inline_assistant_model()
- .context("No inline assistant model")?;
-
let temperature = AgentSettings::temperature_for_model(&model, cx);
+ let mention_set = prompt_editor.read(cx).mention_set().clone();
+ let load_context_task = load_context(&mention_set, cx);
+
Ok(cx.background_spawn(async move {
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
@@ -279,9 +264,9 @@ impl TerminalInlineAssistant {
cache: false,
};
- context_load_task
- .await
- .add_to_request_message(&mut request_message);
+ if let Some(context) = load_context_task.await {
+ context.add_to_request_message(&mut request_message);
+ }
request_message.content.push(prompt.into());
@@ -409,8 +394,6 @@ struct TerminalInlineAssist {
prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>,
codegen: Entity<TerminalCodegen>,
workspace: WeakEntity<Workspace>,
- context_store: Entity<ContextStore>,
- prompt_store: Option<Entity<PromptStore>>,
_subscriptions: Vec<Subscription>,
}
@@ -420,8 +403,6 @@ impl TerminalInlineAssist {
terminal: &Entity<TerminalView>,
prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
workspace: WeakEntity<Workspace>,
- context_store: Entity<ContextStore>,
- prompt_store: Option<Entity<PromptStore>>,
window: &mut Window,
cx: &mut App,
) -> Self {
@@ -431,8 +412,6 @@ impl TerminalInlineAssist {
prompt_editor: Some(prompt_editor.clone()),
codegen: codegen.clone(),
workspace,
- context_store,
- prompt_store,
_subscriptions: vec![
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
TerminalInlineAssistant::update_global(cx, |this, cx| {
@@ -2,7 +2,6 @@ mod acp_onboarding_modal;
mod agent_notification;
mod burn_mode_tooltip;
mod claude_code_onboarding_modal;
-mod context_pill;
mod end_trial_upsell;
mod hold_for_default;
mod onboarding_modal;
@@ -13,7 +12,6 @@ pub use acp_onboarding_modal::*;
pub use agent_notification::*;
pub use burn_mode_tooltip::*;
pub use claude_code_onboarding_modal::*;
-pub use context_pill::*;
pub use end_trial_upsell::*;
pub use hold_for_default::*;
pub use onboarding_modal::*;
@@ -1,858 +0,0 @@
-use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
-
-use file_icons::FileIcons;
-use futures::FutureExt as _;
-use gpui::{
- Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task,
- pulsating_between,
-};
-use language_model::LanguageModelImage;
-use project::Project;
-use prompt_store::PromptStore;
-use rope::Point;
-use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
-use util::paths::PathStyle;
-
-use crate::context::{
- AgentContextHandle, ContextId, ContextKind, DirectoryContextHandle, FetchedUrlContext,
- FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
- SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
-};
-
-#[derive(IntoElement)]
-pub enum ContextPill {
- Added {
- context: AddedContext,
- dupe_name: bool,
- focused: bool,
- on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
- on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
- },
- Suggested {
- name: SharedString,
- icon_path: Option<SharedString>,
- kind: ContextKind,
- focused: bool,
- on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
- },
-}
-
-impl ContextPill {
- pub fn added(
- context: AddedContext,
- dupe_name: bool,
- focused: bool,
- on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
- ) -> Self {
- Self::Added {
- context,
- dupe_name,
- on_remove,
- focused,
- on_click: None,
- }
- }
-
- pub fn suggested(
- name: SharedString,
- icon_path: Option<SharedString>,
- kind: ContextKind,
- focused: bool,
- ) -> Self {
- Self::Suggested {
- name,
- icon_path,
- kind,
- focused,
- on_click: None,
- }
- }
-
- pub fn on_click(mut self, listener: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>) -> Self {
- match &mut self {
- ContextPill::Added { on_click, .. } => {
- *on_click = Some(listener);
- }
- ContextPill::Suggested { on_click, .. } => {
- *on_click = Some(listener);
- }
- }
- self
- }
-
- pub fn id(&self) -> ElementId {
- match self {
- Self::Added { context, .. } => context.handle.element_id("context-pill".into()),
- Self::Suggested { .. } => "suggested-context-pill".into(),
- }
- }
-
- pub fn icon(&self) -> Icon {
- match self {
- Self::Suggested {
- icon_path: Some(icon_path),
- ..
- } => Icon::from_path(icon_path),
- Self::Suggested { kind, .. } => Icon::new(kind.icon()),
- Self::Added { context, .. } => context.icon(),
- }
- }
-}
-
-impl RenderOnce for ContextPill {
- fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
- let color = cx.theme().colors();
-
- let base_pill = h_flex()
- .id(self.id())
- .pl_1()
- .pb(px(1.))
- .border_1()
- .rounded_sm()
- .gap_1()
- .child(self.icon().size(IconSize::XSmall).color(Color::Muted));
-
- match &self {
- ContextPill::Added {
- context,
- dupe_name,
- on_remove,
- focused,
- on_click,
- } => {
- let status_is_error = matches!(context.status, ContextStatus::Error { .. });
- let status_is_warning = matches!(context.status, ContextStatus::Warning { .. });
-
- base_pill
- .pr(if on_remove.is_some() { px(2.) } else { px(4.) })
- .map(|pill| {
- if status_is_error {
- pill.bg(cx.theme().status().error_background)
- .border_color(cx.theme().status().error_border)
- } else if status_is_warning {
- pill.bg(cx.theme().status().warning_background)
- .border_color(cx.theme().status().warning_border)
- } else if *focused {
- pill.bg(color.element_background)
- .border_color(color.border_focused)
- } else {
- pill.bg(color.element_background)
- .border_color(color.border.opacity(0.5))
- }
- })
- .child(
- h_flex()
- .id("context-data")
- .gap_1()
- .child(
- div().max_w_64().child(
- Label::new(context.name.clone())
- .size(LabelSize::Small)
- .truncate(),
- ),
- )
- .when_some(context.parent.as_ref(), |element, parent_name| {
- if *dupe_name {
- element.child(
- Label::new(parent_name.clone())
- .size(LabelSize::XSmall)
- .color(Color::Muted),
- )
- } else {
- element
- }
- })
- .when_some(context.tooltip.as_ref(), |element, tooltip| {
- element.tooltip(Tooltip::text(tooltip.clone()))
- })
- .map(|element| match &context.status {
- ContextStatus::Ready => element
- .when_some(
- context.render_hover.as_ref(),
- |element, render_hover| {
- let render_hover = render_hover.clone();
- element.hoverable_tooltip(move |window, cx| {
- render_hover(window, cx)
- })
- },
- )
- .into_any(),
- ContextStatus::Loading { message } => element
- .tooltip(ui::Tooltip::text(message.clone()))
- .with_animation(
- "pulsating-ctx-pill",
- Animation::new(Duration::from_secs(2))
- .repeat()
- .with_easing(pulsating_between(0.4, 0.8)),
- |label, delta| label.opacity(delta),
- )
- .into_any_element(),
- ContextStatus::Warning { message }
- | ContextStatus::Error { message } => element
- .tooltip(ui::Tooltip::text(message.clone()))
- .into_any_element(),
- }),
- )
- .when_some(on_remove.as_ref(), |element, on_remove| {
- element.child(
- IconButton::new(
- context.handle.element_id("remove".into()),
- IconName::Close,
- )
- .shape(IconButtonShape::Square)
- .icon_size(IconSize::XSmall)
- .tooltip(Tooltip::text("Remove Context"))
- .on_click({
- let on_remove = on_remove.clone();
- move |event, window, cx| on_remove(event, window, cx)
- }),
- )
- })
- .when_some(on_click.as_ref(), |element, on_click| {
- let on_click = on_click.clone();
- element.cursor_pointer().on_click(move |event, window, cx| {
- on_click(event, window, cx);
- cx.stop_propagation();
- })
- })
- .into_any_element()
- }
- ContextPill::Suggested {
- name,
- icon_path: _,
- kind: _,
- focused,
- on_click,
- } => base_pill
- .cursor_pointer()
- .pr_1()
- .border_dashed()
- .map(|pill| {
- if *focused {
- pill.border_color(color.border_focused)
- .bg(color.element_background.opacity(0.5))
- } else {
- pill.border_color(color.border)
- }
- })
- .hover(|style| style.bg(color.element_hover.opacity(0.5)))
- .child(
- div().max_w_64().child(
- Label::new(name.clone())
- .size(LabelSize::Small)
- .color(Color::Muted)
- .truncate(),
- ),
- )
- .tooltip(|_window, cx| {
- Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)
- })
- .when_some(on_click.as_ref(), |element, on_click| {
- let on_click = on_click.clone();
- element.on_click(move |event, window, cx| {
- on_click(event, window, cx);
- cx.stop_propagation();
- })
- })
- .into_any(),
- }
- }
-}
-
-pub enum ContextStatus {
- Ready,
- Loading { message: SharedString },
- Error { message: SharedString },
- Warning { message: SharedString },
-}
-
-#[derive(RegisterComponent)]
-pub struct AddedContext {
- pub handle: AgentContextHandle,
- pub kind: ContextKind,
- pub name: SharedString,
- pub parent: Option<SharedString>,
- pub tooltip: Option<SharedString>,
- pub icon_path: Option<SharedString>,
- pub status: ContextStatus,
- pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
-}
-
-impl AddedContext {
- pub fn icon(&self) -> Icon {
- match &self.status {
- ContextStatus::Warning { .. } => Icon::new(IconName::Warning).color(Color::Warning),
- ContextStatus::Error { .. } => Icon::new(IconName::XCircle).color(Color::Error),
- _ => {
- if let Some(icon_path) = &self.icon_path {
- Icon::from_path(icon_path)
- } else {
- Icon::new(self.kind.icon())
- }
- }
- }
- }
- /// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
- /// `None` if `DirectoryContext` or `RulesContext` no longer exist.
- ///
- /// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
- pub fn new_pending(
- handle: AgentContextHandle,
- prompt_store: Option<&Entity<PromptStore>>,
- project: &Project,
- model: Option<&Arc<dyn language_model::LanguageModel>>,
- cx: &App,
- ) -> Option<AddedContext> {
- match handle {
- AgentContextHandle::File(handle) => {
- Self::pending_file(handle, project.path_style(cx), cx)
- }
- AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
- AgentContextHandle::Symbol(handle) => {
- Self::pending_symbol(handle, project.path_style(cx), cx)
- }
- AgentContextHandle::Selection(handle) => {
- Self::pending_selection(handle, project.path_style(cx), cx)
- }
- AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
- AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
- AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
- AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
- AgentContextHandle::Image(handle) => {
- Some(Self::image(handle, model, project.path_style(cx), cx))
- }
- }
- }
-
- fn pending_file(
- handle: FileContextHandle,
- path_style: PathStyle,
- cx: &App,
- ) -> Option<AddedContext> {
- let full_path = handle
- .buffer
- .read(cx)
- .file()?
- .full_path(cx)
- .to_string_lossy()
- .to_string();
- Some(Self::file(handle, &full_path, path_style, cx))
- }
-
- fn file(
- handle: FileContextHandle,
- full_path: &str,
- path_style: PathStyle,
- cx: &App,
- ) -> AddedContext {
- let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
- AddedContext {
- kind: ContextKind::File,
- name,
- parent,
- tooltip: Some(SharedString::new(full_path)),
- icon_path: FileIcons::get_icon(Path::new(full_path), cx),
- status: ContextStatus::Ready,
- render_hover: None,
- handle: AgentContextHandle::File(handle),
- }
- }
-
- fn pending_directory(
- handle: DirectoryContextHandle,
- project: &Project,
- cx: &App,
- ) -> Option<AddedContext> {
- let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
- let entry = worktree.entry_for_id(handle.entry_id)?;
- let full_path = worktree
- .full_path(&entry.path)
- .to_string_lossy()
- .to_string();
- Some(Self::directory(handle, &full_path, project.path_style(cx)))
- }
-
- fn directory(
- handle: DirectoryContextHandle,
- full_path: &str,
- path_style: PathStyle,
- ) -> AddedContext {
- let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
- AddedContext {
- kind: ContextKind::Directory,
- name,
- parent,
- tooltip: Some(SharedString::new(full_path)),
- icon_path: None,
- status: ContextStatus::Ready,
- render_hover: None,
- handle: AgentContextHandle::Directory(handle),
- }
- }
-
- fn pending_symbol(
- handle: SymbolContextHandle,
- path_style: PathStyle,
- cx: &App,
- ) -> Option<AddedContext> {
- let excerpt = ContextFileExcerpt::new(
- &handle.full_path(cx)?.to_string_lossy(),
- handle.enclosing_line_range(cx),
- path_style,
- cx,
- );
- Some(AddedContext {
- kind: ContextKind::Symbol,
- name: handle.symbol.clone(),
- parent: Some(excerpt.file_name_and_range.clone()),
- tooltip: None,
- icon_path: None,
- status: ContextStatus::Ready,
- render_hover: {
- let handle = handle.clone();
- Some(Rc::new(move |_, cx| {
- excerpt.hover_view(handle.text(cx), cx).into()
- }))
- },
- handle: AgentContextHandle::Symbol(handle),
- })
- }
-
- fn pending_selection(
- handle: SelectionContextHandle,
- path_style: PathStyle,
- cx: &App,
- ) -> Option<AddedContext> {
- let excerpt = ContextFileExcerpt::new(
- &handle.full_path(cx)?.to_string_lossy(),
- handle.line_range(cx),
- path_style,
- cx,
- );
- Some(AddedContext {
- kind: ContextKind::Selection,
- name: excerpt.file_name_and_range.clone(),
- parent: excerpt.parent_name.clone(),
- tooltip: None,
- icon_path: excerpt.icon_path.clone(),
- status: ContextStatus::Ready,
- render_hover: {
- let handle = handle.clone();
- Some(Rc::new(move |_, cx| {
- excerpt.hover_view(handle.text(cx), cx).into()
- }))
- },
- handle: AgentContextHandle::Selection(handle),
- })
- }
-
- fn fetched_url(context: FetchedUrlContext) -> AddedContext {
- AddedContext {
- kind: ContextKind::FetchedUrl,
- name: context.url.clone(),
- parent: None,
- tooltip: None,
- icon_path: None,
- status: ContextStatus::Ready,
- render_hover: None,
- handle: AgentContextHandle::FetchedUrl(context),
- }
- }
-
- fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
- AddedContext {
- kind: ContextKind::Thread,
- name: handle.title(cx),
- parent: None,
- tooltip: None,
- icon_path: None,
- status: if handle.thread.read(cx).is_generating_summary() {
- ContextStatus::Loading {
- message: "Summarizingβ¦".into(),
- }
- } else {
- ContextStatus::Ready
- },
- render_hover: {
- let thread = handle.thread.clone();
- Some(Rc::new(move |_, cx| {
- let text = thread
- .update(cx, |thread, cx| thread.summary(cx))
- .now_or_never()
- .flatten()
- .unwrap_or_else(|| SharedString::from(thread.read(cx).to_markdown()));
- ContextPillHover::new_text(text, cx).into()
- }))
- },
- handle: AgentContextHandle::Thread(handle),
- }
- }
-
- fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext {
- AddedContext {
- kind: ContextKind::TextThread,
- name: handle.title(cx),
- parent: None,
- tooltip: None,
- icon_path: None,
- status: ContextStatus::Ready,
- render_hover: {
- let text_thread = handle.text_thread.clone();
- Some(Rc::new(move |_, cx| {
- let text = text_thread.read(cx).to_xml(cx);
- ContextPillHover::new_text(text.into(), cx).into()
- }))
- },
- handle: AgentContextHandle::TextThread(handle),
- }
- }
-
- fn pending_rules(
- handle: RulesContextHandle,
- prompt_store: Option<&Entity<PromptStore>>,
- cx: &App,
- ) -> Option<AddedContext> {
- let title = prompt_store
- .as_ref()?
- .read(cx)
- .metadata(handle.prompt_id.into())?
- .title
- .unwrap_or_else(|| "Unnamed Rule".into());
- Some(AddedContext {
- kind: ContextKind::Rules,
- name: title,
- parent: None,
- tooltip: None,
- icon_path: None,
- status: ContextStatus::Ready,
- render_hover: None,
- handle: AgentContextHandle::Rules(handle),
- })
- }
-
- fn image(
- context: ImageContext,
- model: Option<&Arc<dyn language_model::LanguageModel>>,
- path_style: PathStyle,
- cx: &App,
- ) -> AddedContext {
- let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
- let (name, parent) =
- extract_file_name_and_directory_from_full_path(full_path, path_style);
- let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
- (name, parent, icon_path)
- } else {
- ("Image".into(), None, None)
- };
-
- let status = match context.status(model) {
- ImageStatus::Loading => ContextStatus::Loading {
- message: "Loadingβ¦".into(),
- },
- ImageStatus::Error => ContextStatus::Error {
- message: "Failed to load Image".into(),
- },
- ImageStatus::Warning => ContextStatus::Warning {
- message: format!(
- "{} doesn't support attaching Images as Context",
- model.map(|m| m.name().0).unwrap_or_else(|| "Model".into())
- )
- .into(),
- },
- ImageStatus::Ready => ContextStatus::Ready,
- };
-
- AddedContext {
- kind: ContextKind::Image,
- name,
- parent,
- tooltip: None,
- icon_path,
- status,
- render_hover: Some(Rc::new({
- let image = context.original_image.clone();
- move |_, cx| {
- let image = image.clone();
- ContextPillHover::new(cx, move |_, _| {
- gpui::img(image.clone())
- .max_w_96()
- .max_h_96()
- .into_any_element()
- })
- .into()
- }
- })),
- handle: AgentContextHandle::Image(context),
- }
- }
-}
-
-fn extract_file_name_and_directory_from_full_path(
- path: &str,
- path_style: PathStyle,
-) -> (SharedString, Option<SharedString>) {
- let (parent, file_name) = path_style.split(path);
- let parent = parent.and_then(|parent| {
- let parent = parent.trim_end_matches(path_style.separator());
- let (_, parent) = path_style.split(parent);
- if parent.is_empty() {
- None
- } else {
- Some(SharedString::new(parent))
- }
- });
- (SharedString::new(file_name), parent)
-}
-
-#[derive(Debug, Clone)]
-struct ContextFileExcerpt {
- pub file_name_and_range: SharedString,
- pub full_path_and_range: SharedString,
- pub parent_name: Option<SharedString>,
- pub icon_path: Option<SharedString>,
-}
-
-impl ContextFileExcerpt {
- pub fn new(full_path: &str, line_range: Range<Point>, path_style: PathStyle, cx: &App) -> Self {
- let (parent, file_name) = path_style.split(full_path);
- let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
- let mut full_path_and_range = full_path.to_owned();
- full_path_and_range.push_str(&line_range_text);
- let mut file_name_and_range = file_name.to_owned();
- file_name_and_range.push_str(&line_range_text);
-
- let parent_name = parent.and_then(|parent| {
- let parent = parent.trim_end_matches(path_style.separator());
- let (_, parent) = path_style.split(parent);
- if parent.is_empty() {
- None
- } else {
- Some(SharedString::new(parent))
- }
- });
-
- let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
-
- ContextFileExcerpt {
- file_name_and_range: file_name_and_range.into(),
- full_path_and_range: full_path_and_range.into(),
- parent_name,
- icon_path,
- }
- }
-
- fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
- let icon_path = self.icon_path.clone();
- let full_path_and_range = self.full_path_and_range.clone();
- ContextPillHover::new(cx, move |_, cx| {
- v_flex()
- .child(
- h_flex()
- .gap_0p5()
- .w_full()
- .max_w_full()
- .border_b_1()
- .border_color(cx.theme().colors().border.opacity(0.6))
- .children(
- icon_path
- .clone()
- .map(Icon::from_path)
- .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
- )
- .child(
- // TODO: make this truncate on the left.
- Label::new(full_path_and_range.clone())
- .size(LabelSize::Small)
- .ml_1(),
- ),
- )
- .child(
- div()
- .id("context-pill-hover-contents")
- .overflow_scroll()
- .max_w_128()
- .max_h_96()
- .child(Label::new(text.clone()).buffer_font(cx)),
- )
- .into_any_element()
- })
- }
-}
-
-struct ContextPillHover {
- render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
-}
-
-impl ContextPillHover {
- fn new(
- cx: &mut App,
- render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
- ) -> Entity<Self> {
- cx.new(|_| Self {
- render_hover: Box::new(render_hover),
- })
- }
-
- fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
- Self::new(cx, move |_, _| {
- div()
- .id("context-pill-hover-contents")
- .overflow_scroll()
- .max_w_128()
- .max_h_96()
- .child(content.clone())
- .into_any_element()
- })
- }
-}
-
-impl Render for ContextPillHover {
- fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- tooltip_container(cx, move |this, cx| {
- this.occlude()
- .on_mouse_move(|_, _, cx| cx.stop_propagation())
- .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
- .child((self.render_hover)(window, cx))
- })
- }
-}
-
-impl Component for AddedContext {
- fn scope() -> ComponentScope {
- ComponentScope::Agent
- }
-
- fn sort_name() -> &'static str {
- "AddedContext"
- }
-
- fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
- let mut next_context_id = ContextId::zero();
- let image_ready = (
- "Ready",
- AddedContext::image(
- ImageContext {
- context_id: next_context_id.post_inc(),
- project_path: None,
- full_path: None,
- original_image: Arc::new(Image::empty()),
- image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
- },
- None,
- PathStyle::local(),
- cx,
- ),
- );
-
- let image_loading = (
- "Loading",
- AddedContext::image(
- ImageContext {
- context_id: next_context_id.post_inc(),
- project_path: None,
- full_path: None,
- original_image: Arc::new(Image::empty()),
- image_task: cx
- .background_spawn(async move {
- smol::Timer::after(Duration::from_secs(60 * 5)).await;
- Some(LanguageModelImage::empty())
- })
- .shared(),
- },
- None,
- PathStyle::local(),
- cx,
- ),
- );
-
- let image_error = (
- "Error",
- AddedContext::image(
- ImageContext {
- context_id: next_context_id.post_inc(),
- project_path: None,
- full_path: None,
- original_image: Arc::new(Image::empty()),
- image_task: Task::ready(None).shared(),
- },
- None,
- PathStyle::local(),
- cx,
- ),
- );
-
- Some(
- v_flex()
- .gap_6()
- .children(
- vec![image_ready, image_loading, image_error]
- .into_iter()
- .map(|(text, context)| {
- single_example(
- text,
- ContextPill::added(context, false, false, None).into_any_element(),
- )
- }),
- )
- .into_any(),
- )
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use gpui::App;
- use language_model::{LanguageModel, fake_provider::FakeLanguageModel};
- use std::sync::Arc;
-
- #[gpui::test]
- fn test_image_context_warning_for_unsupported_model(cx: &mut App) {
- let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel::default());
- assert!(!model.supports_images());
-
- let image_context = ImageContext {
- context_id: ContextId::zero(),
- project_path: None,
- original_image: Arc::new(Image::empty()),
- image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
- full_path: None,
- };
-
- let added_context =
- AddedContext::image(image_context, Some(&model), PathStyle::local(), cx);
-
- assert!(matches!(
- added_context.status,
- ContextStatus::Warning { .. }
- ));
-
- assert!(matches!(added_context.kind, ContextKind::Image));
- assert_eq!(added_context.name.as_ref(), "Image");
- assert!(added_context.parent.is_none());
- assert!(added_context.icon_path.is_none());
- }
-
- #[gpui::test]
- fn test_image_context_ready_for_no_model(cx: &mut App) {
- let image_context = ImageContext {
- context_id: ContextId::zero(),
- project_path: None,
- original_image: Arc::new(Image::empty()),
- image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
- full_path: None,
- };
-
- let added_context = AddedContext::image(image_context, None, PathStyle::local(), cx);
-
- assert!(
- matches!(added_context.status, ContextStatus::Ready),
- "Expected ready status when no model provided"
- );
-
- assert!(matches!(added_context.kind, ContextKind::Image));
- assert_eq!(added_context.name.as_ref(), "Image");
- assert!(added_context.parent.is_none());
- assert!(added_context.icon_path.is_none());
- }
-}