Detailed changes
@@ -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<ContextStore>,
+ workspace: Entity<Workspace>,
+ 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::<Editor>())
+ {
+ 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::<AssistantPanel>(cx) {
+ panel.update(cx, |panel, cx| {
+ panel
+ .open_thread(&thread_id, window, cx)
+ .detach_and_log_err(cx)
+ });
+ }
+ })
+ }
+ }
+}
@@ -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<Path>,
+ pub path: ProjectPath,
pub context_buffers: Vec<ContextBuffer>,
pub snapshot: ContextSnapshot,
}
@@ -185,17 +184,18 @@ impl FileContext {
impl DirectoryContext {
pub fn new(
id: ContextId,
- path: &Path,
+ project_path: ProjectPath,
context_buffers: Vec<ContextBuffer>,
) -> 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,
@@ -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<ContextBuffer>) {
+ fn insert_directory(&mut self, project_path: ProjectPath, context_buffers: Vec<ContextBuffer>) {
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();
@@ -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(
@@ -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,