From fcadcbb510d13991b5c36aea5b5bc7af4b608517 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 28 Mar 2025 20:05:30 +0100 Subject: [PATCH] assistant2: Make context pills clickable (#27680) Release Notes: - N/A --- crates/assistant2/src/active_thread.rs | 174 +++++++++++++++++++---- crates/assistant2/src/context.rs | 16 +-- crates/assistant2/src/context_store.rs | 19 ++- crates/assistant2/src/context_strip.rs | 29 +++- crates/assistant2/src/ui/context_pill.rs | 4 +- 5 files changed, 189 insertions(+), 53 deletions(-) diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs index adf8c1ad0235b7a32d31a354f448fdd8e9bd94d5..6da69b8128d248a70aca9a1fcd1c0a41ce0d5645 100644 --- a/crates/assistant2/src/active_thread.rs +++ b/crates/assistant2/src/active_thread.rs @@ -1,3 +1,4 @@ +use crate::context::{AssistantContext, ContextId}; use crate::thread::{ LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError, ThreadEvent, ThreadFeedback, @@ -19,9 +20,12 @@ use gpui::{ use language::{Buffer, LanguageRegistry}; use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role}; use markdown::{Markdown, MarkdownStyle}; +use project::ProjectItem as _; use settings::Settings as _; +use std::rc::Rc; use std::sync::Arc; use std::time::Duration; +use text::ToPoint; use theme::ThemeSettings; use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip}; use util::ResultExt as _; @@ -778,6 +782,9 @@ impl ActiveThread { return Empty.into_any(); }; + let context_store = self.context_store.clone(); + let workspace = self.workspace.clone(); + let thread = self.thread.read(cx); // Get all the data we need from thread before we start using it in closures let checkpoint = thread.checkpoint_for_message(message_id); @@ -901,36 +908,53 @@ impl ActiveThread { .into_any_element(), }; - let message_content = v_flex() - .gap_1p5() - .child( - if let Some(edit_message_editor) = edit_message_editor.clone() { - div() - .key_context("EditMessageEditor") - .on_action(cx.listener(Self::cancel_editing_message)) - .on_action(cx.listener(Self::confirm_editing_message)) - .min_h_6() - .child(edit_message_editor) - } else { - div() - .min_h_6() - .text_ui(cx) - .child(self.render_message_content(message_id, rendered_message, cx)) - }, - ) - .when_some(context, |parent, context| { - if !context.is_empty() { - parent.child( - h_flex().flex_wrap().gap_1().children( - context - .into_iter() - .map(|context| ContextPill::added(context, false, false, None)), - ), - ) - } else { - parent - } - }); + let message_content = + v_flex() + .gap_1p5() + .child( + if let Some(edit_message_editor) = edit_message_editor.clone() { + div() + .key_context("EditMessageEditor") + .on_action(cx.listener(Self::cancel_editing_message)) + .on_action(cx.listener(Self::confirm_editing_message)) + .min_h_6() + .child(edit_message_editor) + } else { + div() + .min_h_6() + .text_ui(cx) + .child(self.render_message_content(message_id, rendered_message, cx)) + }, + ) + .when_some(context, |parent, context| { + if !context.is_empty() { + parent.child(h_flex().flex_wrap().gap_1().children( + context.into_iter().map(|context| { + let context_id = context.id; + ContextPill::added(context, false, false, None).on_click(Rc::new( + cx.listener({ + let workspace = workspace.clone(); + let context_store = context_store.clone(); + move |_, _, window, cx| { + if let Some(workspace) = workspace.upgrade() { + open_context( + context_id, + context_store.clone(), + workspace, + window, + cx, + ); + cx.notify(); + } + } + }), + )) + }), + )) + } else { + parent + } + }); let styled_message = match message.role { Role::User => v_flex() @@ -1823,3 +1847,93 @@ impl Render for ActiveThread { .child(self.render_vertical_scrollbar(cx)) } } + +pub(crate) fn open_context( + id: ContextId, + context_store: Entity, + workspace: Entity, + window: &mut Window, + cx: &mut App, +) { + let Some(context) = context_store.read(cx).context_for_id(id) else { + return; + }; + + match context { + AssistantContext::File(file_context) => { + if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx) + { + workspace.update(cx, |workspace, cx| { + workspace + .open_path(project_path, None, true, window, cx) + .detach_and_log_err(cx); + }); + } + } + AssistantContext::Directory(directory_context) => { + let path = directory_context.path.clone(); + workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + if let Some(entry) = project.entry_for_path(&path, cx) { + cx.emit(project::Event::RevealInProjectPanel(entry.id)); + } + }) + }) + } + AssistantContext::Symbol(symbol_context) => { + if let Some(project_path) = symbol_context + .context_symbol + .buffer + .read(cx) + .project_path(cx) + { + let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot(); + let target_position = symbol_context + .context_symbol + .id + .range + .start + .to_point(&snapshot); + + 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::()) + { + active_editor + .downgrade() + .update_in(cx, |editor, window, cx| { + editor.go_to_singleton_buffer_point( + target_position, + window, + cx, + ); + }) + .log_err(); + } + }) + .detach(); + } + } + AssistantContext::FetchedUrl(fetched_url_context) => { + cx.open_url(&fetched_url_context.url); + } + AssistantContext::Thread(thread_context) => { + let thread_id = thread_context.thread.read(cx).id().clone(); + workspace.update(cx, |workspace, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel + .open_thread(&thread_id, window, cx) + .detach_and_log_err(cx) + }); + } + }) + } + } +} diff --git a/crates/assistant2/src/context.rs b/crates/assistant2/src/context.rs index d86542210e8e12f40134f38084b0d038705b739b..bea281f260f1da253e8c1df4c00e8ca7bb0bfd52 100644 --- a/crates/assistant2/src/context.rs +++ b/crates/assistant2/src/context.rs @@ -1,5 +1,4 @@ -use std::rc::Rc; -use std::{ops::Range, path::Path}; +use std::ops::Range; use file_icons::FileIcons; use gpui::{App, Entity, SharedString}; @@ -85,7 +84,7 @@ pub struct FileContext { #[derive(Debug)] pub struct DirectoryContext { - pub path: Rc, + pub path: ProjectPath, pub context_buffers: Vec, pub snapshot: ContextSnapshot, } @@ -185,17 +184,18 @@ impl FileContext { impl DirectoryContext { pub fn new( id: ContextId, - path: &Path, + project_path: ProjectPath, context_buffers: Vec, ) -> DirectoryContext { - let full_path: SharedString = path.to_string_lossy().into_owned().into(); + let full_path: SharedString = project_path.path.to_string_lossy().into_owned().into(); - let name = match path.file_name() { + let name = match project_path.path.file_name() { Some(name) => name.to_string_lossy().into_owned().into(), None => full_path.clone(), }; - let parent = path + let parent = project_path + .path .parent() .and_then(|p| p.file_name()) .map(|p| p.to_string_lossy().into_owned().into()); @@ -208,7 +208,7 @@ impl DirectoryContext { .into(); DirectoryContext { - path: path.into(), + path: project_path, context_buffers, snapshot: ContextSnapshot { id, diff --git a/crates/assistant2/src/context_store.rs b/crates/assistant2/src/context_store.rs index 7177f2114acee27c8ed45f133c6b2c8b088a58a5..1a08c4cb2268dd346a26466911f65d3dc05cad5e 100644 --- a/crates/assistant2/src/context_store.rs +++ b/crates/assistant2/src/context_store.rs @@ -60,6 +60,10 @@ impl ContextStore { &self.context } + pub fn context_for_id(&self, id: ContextId) -> Option<&AssistantContext> { + self.context().iter().find(|context| context.id() == id) + } + pub fn clear(&mut self) { self.context.clear(); self.files.clear(); @@ -253,21 +257,21 @@ impl ContextStore { } this.update(cx, |this, _| { - this.insert_directory(&project_path.path, context_buffers); + this.insert_directory(project_path, context_buffers); })?; anyhow::Ok(()) }) } - fn insert_directory(&mut self, path: &Path, context_buffers: Vec) { + fn insert_directory(&mut self, project_path: ProjectPath, context_buffers: Vec) { let id = self.next_context_id.post_inc(); - self.directories.insert(path.to_path_buf(), id); + self.directories.insert(project_path.path.to_path_buf(), id); self.context .push(AssistantContext::Directory(DirectoryContext::new( id, - path, + project_path, context_buffers, ))); } @@ -704,8 +708,9 @@ pub fn refresh_context_store_text( || changed_buffers.iter().any(|buffer| { let buffer = buffer.read(cx); - buffer_path_log_err(&buffer) - .map_or(false, |path| path.starts_with(&directory_context.path)) + buffer_path_log_err(&buffer).map_or(false, |path| { + path.starts_with(&directory_context.path.path) + }) }); if should_refresh { @@ -797,7 +802,7 @@ fn refresh_directory_text( let context_buffers = context_buffers.await; context_store .update(cx, |context_store, _| { - let new_directory_context = DirectoryContext::new(id, &path, context_buffers); + let new_directory_context = DirectoryContext::new(id, path, context_buffers); context_store.replace_context(AssistantContext::Directory(new_directory_context)); }) .ok(); diff --git a/crates/assistant2/src/context_strip.rs b/crates/assistant2/src/context_strip.rs index 9a567809670a991051d01c58db783702569b8f86..63d4ce6cbbd3304c0933e32dcac8633b86d95167 100644 --- a/crates/assistant2/src/context_strip.rs +++ b/crates/assistant2/src/context_strip.rs @@ -4,15 +4,15 @@ use collections::HashSet; use editor::Editor; use file_icons::FileIcons; use gpui::{ - App, Bounds, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, - WeakEntity, + App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, + Subscription, WeakEntity, }; use itertools::Itertools; use language::Buffer; use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip}; use workspace::{notifications::NotifyResultExt, Workspace}; -use crate::context::ContextKind; +use crate::context::{ContextId, ContextKind}; use crate::context_picker::{ConfirmBehavior, ContextPicker}; use crate::context_store::ContextStore; use crate::thread::Thread; @@ -277,6 +277,14 @@ impl ContextStrip { best.map(|(index, _, _)| index) } + fn open_context(&mut self, id: ContextId, window: &mut Window, cx: &mut App) { + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + + crate::active_thread::open_context(id, self.context_store.clone(), workspace, window, cx); + } + fn remove_focused_context( &mut self, _: &RemoveFocusedContext, @@ -458,6 +466,7 @@ impl Render for ContextStrip { } }) .children(context.iter().enumerate().map(|(i, context)| { + let id = context.id; ContextPill::added( context.clone(), dupe_names.contains(&context.name), @@ -473,10 +482,16 @@ impl Render for ContextStrip { })) }), ) - .on_click(Rc::new(cx.listener(move |this, _, _window, cx| { - this.focused_index = Some(i); - cx.notify(); - }))) + .on_click(Rc::new(cx.listener( + move |this, event: &ClickEvent, window, cx| { + if event.down.click_count > 1 { + this.open_context(id, window, cx); + } else { + this.focused_index = Some(i); + } + cx.notify(); + }, + ))) })) .when_some(suggested_context, |el, suggested| { el.child( diff --git a/crates/assistant2/src/ui/context_pill.rs b/crates/assistant2/src/ui/context_pill.rs index 5fac7c3758e0e513fd76318fc340e9a8d88b32a6..d2ec1d625f60531895dc7ea3bfd588bef0ff6ef4 100644 --- a/crates/assistant2/src/ui/context_pill.rs +++ b/crates/assistant2/src/ui/context_pill.rs @@ -162,7 +162,9 @@ impl RenderOnce for ContextPill { }) .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)) + element + .cursor_pointer() + .on_click(move |event, window, cx| on_click(event, window, cx)) }), ContextPill::Suggested { name,