Detailed changes
@@ -76,6 +76,10 @@ impl ActiveThread {
self.thread.read(cx).summary()
}
+ pub fn summary_or_default(&self, cx: &AppContext) -> SharedString {
+ self.thread.read(cx).summary_or_default()
+ }
+
pub fn last_error(&self) -> Option<ThreadError> {
self.last_error.clone()
}
@@ -300,11 +300,12 @@ impl AssistantPanel {
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
- let title = if self.thread.read(cx).is_empty() {
- SharedString::from("New Thread")
+ let thread = self.thread.read(cx);
+
+ let title = if thread.is_empty() {
+ thread.summary_or_default(cx)
} else {
- self.thread
- .read(cx)
+ thread
.summary(cx)
.unwrap_or_else(|| SharedString::from("Loading Summaryβ¦"))
};
@@ -43,6 +43,24 @@ pub enum ContextKind {
}
impl ContextKind {
+ pub fn all() -> &'static [ContextKind] {
+ &[
+ ContextKind::File,
+ ContextKind::Directory,
+ ContextKind::FetchedUrl,
+ ContextKind::Thread,
+ ]
+ }
+
+ pub fn label(&self) -> &'static str {
+ match self {
+ ContextKind::File => "File",
+ ContextKind::Directory => "Folder",
+ ContextKind::FetchedUrl => "Fetch",
+ ContextKind::Thread => "Thread",
+ }
+ }
+
pub fn icon(&self) -> IconName {
match self {
ContextKind::File => IconName::File,
@@ -3,15 +3,17 @@ mod fetch_context_picker;
mod file_context_picker;
mod thread_context_picker;
+use std::path::PathBuf;
use std::sync::Arc;
+use editor::Editor;
+use file_context_picker::render_file_context_entry;
use gpui::{
- AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
- WeakModel, WeakView,
+ AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakModel, WeakView,
};
-use picker::{Picker, PickerDelegate};
-use ui::{prelude::*, ListItem, ListItemSpacing};
-use util::ResultExt;
+use project::ProjectPath;
+use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
+use ui::{prelude::*, ContextMenu, ContextMenuEntry, ContextMenuItem};
use workspace::Workspace;
use crate::context::ContextKind;
@@ -21,6 +23,7 @@ use crate::context_picker::file_context_picker::FileContextPicker;
use crate::context_picker::thread_context_picker::ThreadContextPicker;
use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore;
+use crate::AssistantPanel;
#[derive(Debug, Clone, Copy)]
pub enum ConfirmBehavior {
@@ -30,7 +33,7 @@ pub enum ConfirmBehavior {
#[derive(Debug, Clone)]
enum ContextPickerMode {
- Default,
+ Default(View<ContextMenu>),
File(View<FileContextPicker>),
Directory(View<DirectoryContextPicker>),
Fetch(View<FetchContextPicker>),
@@ -39,7 +42,10 @@ enum ContextPickerMode {
pub(super) struct ContextPicker {
mode: ContextPickerMode,
- picker: View<Picker<ContextPickerDelegate>>,
+ workspace: WeakView<Workspace>,
+ context_store: WeakModel<ContextStore>,
+ thread_store: Option<WeakModel<ThreadStore>>,
+ confirm_behavior: ConfirmBehavior,
}
impl ContextPicker {
@@ -50,53 +56,287 @@ impl ContextPicker {
confirm_behavior: ConfirmBehavior,
cx: &mut ViewContext<Self>,
) -> Self {
- let mut entries = Vec::new();
- entries.push(ContextPickerEntry {
- name: "File".into(),
- kind: ContextKind::File,
- icon: IconName::File,
- });
- entries.push(ContextPickerEntry {
- name: "Folder".into(),
- kind: ContextKind::Directory,
- icon: IconName::Folder,
+ ContextPicker {
+ mode: ContextPickerMode::Default(ContextMenu::build(cx, |menu, _cx| menu)),
+ workspace,
+ context_store,
+ thread_store,
+ confirm_behavior,
+ }
+ }
+
+ pub fn reset_mode(&mut self, cx: &mut ViewContext<Self>) {
+ self.mode = ContextPickerMode::Default(self.build(cx));
+ }
+
+ fn build(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
+ let context_picker = cx.view().clone();
+
+ ContextMenu::build(cx, move |menu, cx| {
+ let kind_entry = |kind: &'static ContextKind| {
+ let context_picker = context_picker.clone();
+
+ ContextMenuEntry::new(kind.label())
+ .icon(kind.icon())
+ .handler(move |cx| {
+ context_picker.update(cx, |this, cx| this.select_kind(*kind, 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));
+
+ menu.when(has_recent, |menu| menu.label("Recent"))
+ .extend(recent_entries)
+ .when(has_recent, |menu| menu.separator())
+ .extend(ContextKind::all().into_iter().map(kind_entry))
+ })
+ }
+
+ fn select_kind(&mut self, kind: ContextKind, cx: &mut ViewContext<Self>) {
+ let context_picker = cx.view().downgrade();
+
+ match kind {
+ ContextKind::File => {
+ self.mode = ContextPickerMode::File(cx.new_view(|cx| {
+ FileContextPicker::new(
+ context_picker.clone(),
+ self.workspace.clone(),
+ self.context_store.clone(),
+ self.confirm_behavior,
+ cx,
+ )
+ }));
+ }
+ ContextKind::Directory => {
+ self.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
+ DirectoryContextPicker::new(
+ context_picker.clone(),
+ self.workspace.clone(),
+ self.context_store.clone(),
+ self.confirm_behavior,
+ cx,
+ )
+ }));
+ }
+ ContextKind::FetchedUrl => {
+ self.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
+ FetchContextPicker::new(
+ context_picker.clone(),
+ self.workspace.clone(),
+ self.context_store.clone(),
+ self.confirm_behavior,
+ cx,
+ )
+ }));
+ }
+ ContextKind::Thread => {
+ if let Some(thread_store) = self.thread_store.as_ref() {
+ self.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
+ ThreadContextPicker::new(
+ thread_store.clone(),
+ context_picker.clone(),
+ self.context_store.clone(),
+ self.confirm_behavior,
+ cx,
+ )
+ }));
+ }
+ }
+ }
+
+ cx.notify();
+ cx.focus_self();
+ }
+
+ fn recent_menu_item(
+ &self,
+ context_picker: View<ContextPicker>,
+ ix: usize,
+ entry: RecentEntry,
+ ) -> ContextMenuItem {
+ match entry {
+ RecentEntry::File {
+ project_path,
+ path_prefix,
+ } => {
+ let context_store = self.context_store.clone();
+ let path = project_path.path.clone();
+
+ ContextMenuItem::custom_entry(
+ move |cx| {
+ render_file_context_entry(
+ ElementId::NamedInteger("ctx-recent".into(), ix),
+ &path,
+ &path_prefix,
+ context_store.clone(),
+ cx,
+ )
+ .into_any()
+ },
+ move |cx| {
+ context_picker.update(cx, |this, cx| {
+ this.add_recent_file(project_path.clone(), cx);
+ })
+ },
+ )
+ }
+ RecentEntry::Thread(thread) => {
+ let context_store = self.context_store.clone();
+ let view_thread = thread.clone();
+
+ ContextMenuItem::custom_entry(
+ move |cx| {
+ render_thread_context_entry(&view_thread, context_store.clone(), cx)
+ .into_any()
+ },
+ move |cx| {
+ context_picker.update(cx, |this, cx| {
+ this.add_recent_thread(thread.clone(), cx);
+ })
+ },
+ )
+ }
+ }
+ }
+
+ fn add_recent_file(&self, project_path: ProjectPath, cx: &mut ViewContext<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(), cx)
});
- entries.push(ContextPickerEntry {
- name: "Fetch".into(),
- kind: ContextKind::FetchedUrl,
- icon: IconName::Globe,
+
+ let workspace = self.workspace.clone();
+
+ cx.spawn(|_, mut cx| async move {
+ match task.await {
+ Ok(_) => {
+ return anyhow::Ok(());
+ }
+ Err(err) => {
+ let Some(workspace) = workspace.upgrade() else {
+ return anyhow::Ok(());
+ };
+
+ workspace.update(&mut cx, |workspace, cx| {
+ workspace.show_error(&err, cx);
+ })
+ }
+ }
+ })
+ .detach_and_log_err(cx);
+
+ cx.notify();
+ }
+
+ fn add_recent_thread(&self, thread: ThreadContextEntry, cx: &mut ViewContext<Self>) {
+ let Some(context_store) = self.context_store.upgrade() else {
+ return;
+ };
+
+ let Some(thread) = self
+ .thread_store
+ .clone()
+ .and_then(|this| this.upgrade())
+ .and_then(|this| this.update(cx, |this, cx| this.open_thread(&thread.id, cx)))
+ else {
+ return;
+ };
+
+ context_store.update(cx, |context_store, cx| {
+ context_store.add_thread(thread, cx);
});
- if thread_store.is_some() {
- entries.push(ContextPickerEntry {
- name: "Thread".into(),
- kind: ContextKind::Thread,
- icon: IconName::MessageCircle,
- });
+ cx.notify();
+ }
+
+ fn recent_entries(&self, cx: &mut WindowContext) -> Vec<RecentEntry> {
+ let Some(workspace) = self.workspace.upgrade().map(|w| w.read(cx)) else {
+ return vec![];
+ };
+
+ let Some(context_store) = self.context_store.upgrade().map(|cs| cs.read(cx)) else {
+ return vec![];
+ };
+
+ let mut recent = Vec::with_capacity(6);
+
+ let mut current_files = context_store.file_paths(cx);
+
+ if let Some(active_path) = Self::active_singleton_buffer_path(&workspace, cx) {
+ current_files.insert(active_path);
}
- let delegate = ContextPickerDelegate {
- context_picker: cx.view().downgrade(),
- workspace,
- thread_store,
- context_store,
- confirm_behavior,
- entries,
- selected_ix: 0,
+ let project = workspace.project().read(cx);
+
+ recent.extend(
+ workspace
+ .recent_navigation_history_iter(cx)
+ .filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
+ .take(4)
+ .filter_map(|(project_path, _)| {
+ project
+ .worktree_for_id(project_path.worktree_id, cx)
+ .map(|worktree| RecentEntry::File {
+ project_path,
+ path_prefix: worktree.read(cx).root_name().into(),
+ })
+ }),
+ );
+
+ let mut current_threads = context_store.thread_ids();
+
+ if let Some(active_thread) = workspace
+ .panel::<AssistantPanel>(cx)
+ .map(|panel| panel.read(cx).active_thread(cx))
+ {
+ current_threads.insert(active_thread.read(cx).id().clone());
+ }
+
+ let Some(thread_store) = self
+ .thread_store
+ .as_ref()
+ .and_then(|thread_store| thread_store.upgrade())
+ else {
+ return recent;
};
- let picker = cx.new_view(|cx| {
- Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
+ thread_store.update(cx, |thread_store, cx| {
+ recent.extend(
+ thread_store
+ .threads(cx)
+ .into_iter()
+ .filter(|thread| !current_threads.contains(thread.read(cx).id()))
+ .take(2)
+ .map(|thread| {
+ let thread = thread.read(cx);
+
+ RecentEntry::Thread(ThreadContextEntry {
+ id: thread.id().clone(),
+ summary: thread.summary_or_default(),
+ })
+ }),
+ )
});
- ContextPicker {
- mode: ContextPickerMode::Default,
- picker,
- }
+ recent
}
- pub fn reset_mode(&mut self) {
- self.mode = ContextPickerMode::Default;
+ fn active_singleton_buffer_path(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
+ let active_item = workspace.active_item(cx)?;
+
+ let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
+ let buffer = editor.buffer().read(cx).as_singleton()?;
+
+ let path = buffer.read(cx).file()?.path().to_path_buf();
+ Some(path)
}
}
@@ -105,7 +345,7 @@ impl EventEmitter<DismissEvent> for ContextPicker {}
impl FocusableView for ContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
match &self.mode {
- ContextPickerMode::Default => self.picker.focus_handle(cx),
+ ContextPickerMode::Default(menu) => menu.focus_handle(cx),
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx),
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
@@ -120,7 +360,7 @@ impl Render for ContextPicker {
.w(px(400.))
.min_w(px(400.))
.map(|parent| match &self.mode {
- ContextPickerMode::Default => parent.child(self.picker.clone()),
+ ContextPickerMode::Default(menu) => parent.child(menu.clone()),
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerMode::Directory(directory_picker) => {
parent.child(directory_picker.clone())
@@ -130,140 +370,10 @@ impl Render for ContextPicker {
})
}
}
-
-#[derive(Clone)]
-struct ContextPickerEntry {
- name: SharedString,
- kind: ContextKind,
- icon: IconName,
-}
-
-pub(crate) struct ContextPickerDelegate {
- context_picker: WeakView<ContextPicker>,
- workspace: WeakView<Workspace>,
- thread_store: Option<WeakModel<ThreadStore>>,
- context_store: WeakModel<ContextStore>,
- confirm_behavior: ConfirmBehavior,
- entries: Vec<ContextPickerEntry>,
- selected_ix: usize,
-}
-
-impl PickerDelegate for ContextPickerDelegate {
- type ListItem = ListItem;
-
- fn match_count(&self) -> usize {
- self.entries.len()
- }
-
- fn selected_index(&self) -> usize {
- self.selected_ix
- }
-
- fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
- self.selected_ix = ix.min(self.entries.len().saturating_sub(1));
- cx.notify();
- }
-
- fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
- "Select a context sourceβ¦".into()
- }
-
- fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
- Task::ready(())
- }
-
- fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
- if let Some(entry) = self.entries.get(self.selected_ix) {
- self.context_picker
- .update(cx, |this, cx| {
- match entry.kind {
- ContextKind::File => {
- this.mode = ContextPickerMode::File(cx.new_view(|cx| {
- FileContextPicker::new(
- self.context_picker.clone(),
- self.workspace.clone(),
- self.context_store.clone(),
- self.confirm_behavior,
- cx,
- )
- }));
- }
- ContextKind::Directory => {
- this.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
- DirectoryContextPicker::new(
- self.context_picker.clone(),
- self.workspace.clone(),
- self.context_store.clone(),
- self.confirm_behavior,
- cx,
- )
- }));
- }
- ContextKind::FetchedUrl => {
- this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
- FetchContextPicker::new(
- self.context_picker.clone(),
- self.workspace.clone(),
- self.context_store.clone(),
- self.confirm_behavior,
- cx,
- )
- }));
- }
- ContextKind::Thread => {
- if let Some(thread_store) = self.thread_store.as_ref() {
- this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
- ThreadContextPicker::new(
- thread_store.clone(),
- self.context_picker.clone(),
- self.context_store.clone(),
- self.confirm_behavior,
- cx,
- )
- }));
- }
- }
- }
-
- cx.focus_self();
- })
- .log_err();
- }
- }
-
- fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
- self.context_picker
- .update(cx, |this, cx| match this.mode {
- ContextPickerMode::Default => cx.emit(DismissEvent),
- ContextPickerMode::File(_)
- | ContextPickerMode::Directory(_)
- | ContextPickerMode::Fetch(_)
- | ContextPickerMode::Thread(_) => {}
- })
- .log_err();
- }
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- _cx: &mut ViewContext<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let entry = &self.entries[ix];
-
- Some(
- ListItem::new(ix)
- .inset(true)
- .spacing(ListItemSpacing::Dense)
- .toggle_state(selected)
- .child(
- h_flex()
- .min_w(px(250.))
- .max_w(px(400.))
- .gap_2()
- .child(Icon::new(entry.icon).size(IconSize::Small))
- .child(Label::new(entry.name.clone()).single_line()),
- ),
- )
- }
+enum RecentEntry {
+ File {
+ project_path: ProjectPath,
+ path_prefix: Arc<str>,
+ },
+ Thread(ThreadContextEntry),
}
@@ -222,7 +222,7 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
- this.reset_mode();
+ this.reset_mode(cx);
cx.emit(DismissEvent);
})
.ok();
@@ -225,7 +225,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
- this.reset_mode();
+ this.reset_mode(cx);
cx.emit(DismissEvent);
})
.ok();
@@ -4,7 +4,9 @@ use std::sync::Arc;
use file_icons::FileIcons;
use fuzzy::PathMatch;
-use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
+use gpui::{
+ AppContext, DismissEvent, FocusHandle, FocusableView, Stateful, Task, View, WeakModel, WeakView,
+};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{prelude::*, ListItem, Tooltip};
@@ -238,7 +240,7 @@ impl PickerDelegate for FileContextPickerDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
- this.reset_mode();
+ this.reset_mode(cx);
cx.emit(DismissEvent);
})
.ok();
@@ -252,82 +254,97 @@ impl PickerDelegate for FileContextPickerDelegate {
) -> Option<Self::ListItem> {
let path_match = &self.matches[ix];
- let (file_name, directory) = if path_match.path.as_ref() == Path::new("") {
- (SharedString::from(path_match.path_prefix.clone()), None)
- } else {
- let file_name = path_match
- .path
- .file_name()
- .unwrap_or_default()
- .to_string_lossy()
- .to_string()
- .into();
-
- let mut directory = format!("{}/", path_match.path_prefix);
- if let Some(parent) = path_match
- .path
- .parent()
- .filter(|parent| parent != &Path::new(""))
- {
- directory.push_str(&parent.to_string_lossy());
- directory.push('/');
- }
-
- (file_name, Some(directory))
- };
-
- let added = self.context_store.upgrade().and_then(|context_store| {
- context_store
- .read(cx)
- .will_include_file_path(&path_match.path, cx)
- });
-
- let file_icon = FileIcons::get_icon(&path_match.path.clone(), cx)
- .map(Icon::from_path)
- .unwrap_or_else(|| Icon::new(IconName::File));
-
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
- .child(
- h_flex()
- .gap_2()
- .child(file_icon.size(IconSize::Small))
- .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.end_slot(
- h_flex()
- .gap_1()
- .child(
- Icon::new(IconName::Check)
- .size(IconSize::Small)
- .color(Color::Success),
- )
- .child(Label::new("Added").size(LabelSize::Small)),
- ),
- FileInclusion::InDirectory(dir_name) => {
- let dir_name = dir_name.to_string_lossy().into_owned();
-
- el.end_slot(
- h_flex()
- .gap_1()
- .child(
- Icon::new(IconName::Check)
- .size(IconSize::Small)
- .color(Color::Success),
- )
- .child(Label::new("Included").size(LabelSize::Small)),
- )
- .tooltip(move |cx| Tooltip::text(format!("in {dir_name}"), cx))
- }
- }),
+ .child(render_file_context_entry(
+ ElementId::NamedInteger("file-ctx-picker".into(), ix),
+ &path_match.path,
+ &path_match.path_prefix,
+ self.context_store.clone(),
+ cx,
+ )),
)
}
}
+
+pub fn render_file_context_entry(
+ id: ElementId,
+ path: &Path,
+ path_prefix: &Arc<str>,
+ context_store: WeakModel<ContextStore>,
+ cx: &WindowContext,
+) -> Stateful<Div> {
+ let (file_name, directory) = if path == Path::new("") {
+ (SharedString::from(path_prefix.clone()), None)
+ } else {
+ let file_name = path
+ .file_name()
+ .unwrap_or_default()
+ .to_string_lossy()
+ .to_string()
+ .into();
+
+ let mut directory = format!("{}/", path_prefix);
+
+ if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
+ directory.push_str(&parent.to_string_lossy());
+ directory.push('/');
+ }
+
+ (file_name, Some(directory))
+ };
+
+ let added = context_store
+ .upgrade()
+ .and_then(|context_store| context_store.read(cx).will_include_file_path(path, cx));
+
+ let file_icon = FileIcons::get_icon(&path, cx)
+ .map(Icon::from_path)
+ .unwrap_or_else(|| Icon::new(IconName::File));
+
+ h_flex()
+ .id(id)
+ .gap_1()
+ .w_full()
+ .child(file_icon.size(IconSize::Small))
+ .child(
+ h_flex()
+ .gap_2()
+ .child(Label::new(file_name))
+ .children(directory.map(|directory| {
+ Label::new(directory)
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ })),
+ )
+ .child(div().w_full())
+ .when_some(added, |el, added| match added {
+ FileInclusion::Direct(_) => el.child(
+ h_flex()
+ .gap_1()
+ .child(
+ Icon::new(IconName::Check)
+ .size(IconSize::Small)
+ .color(Color::Success),
+ )
+ .child(Label::new("Added").size(LabelSize::Small)),
+ ),
+ FileInclusion::InDirectory(dir_name) => {
+ let dir_name = dir_name.to_string_lossy().into_owned();
+
+ el.child(
+ h_flex()
+ .gap_1()
+ .child(
+ Icon::new(IconName::Check)
+ .size(IconSize::Small)
+ .color(Color::Success),
+ )
+ .child(Label::new("Included").size(LabelSize::Small)),
+ )
+ .tooltip(move |cx| Tooltip::text(format!("in {dir_name}"), cx))
+ }
+ })
+}
@@ -6,7 +6,7 @@ use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem};
use crate::context_picker::{ConfirmBehavior, ContextPicker};
-use crate::context_store;
+use crate::context_store::{self, ContextStore};
use crate::thread::ThreadId;
use crate::thread_store::ThreadStore;
@@ -47,9 +47,9 @@ impl Render for ThreadContextPicker {
}
#[derive(Debug, Clone)]
-struct ThreadContextEntry {
- id: ThreadId,
- summary: SharedString,
+pub struct ThreadContextEntry {
+ pub id: ThreadId,
+ pub summary: SharedString,
}
pub struct ThreadContextPickerDelegate {
@@ -103,10 +103,8 @@ impl PickerDelegate for ThreadContextPickerDelegate {
this.threads(cx)
.into_iter()
.map(|thread| {
- const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
-
let id = thread.read(cx).id().clone();
- let summary = thread.read(cx).summary().unwrap_or(DEFAULT_SUMMARY);
+ let summary = thread.read(cx).summary_or_default();
ThreadContextEntry { id, summary }
})
.collect::<Vec<_>>()
@@ -179,7 +177,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
- this.reset_mode();
+ this.reset_mode(cx);
cx.emit(DismissEvent);
})
.ok();
@@ -193,27 +191,37 @@ impl PickerDelegate for ThreadContextPickerDelegate {
) -> Option<Self::ListItem> {
let thread = &self.matches[ix];
- let added = self.context_store.upgrade().map_or(false, |context_store| {
- context_store.read(cx).includes_thread(&thread.id).is_some()
- });
+ Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
+ render_thread_context_entry(thread, self.context_store.clone(), cx),
+ ))
+ }
+}
- Some(
- ListItem::new(ix)
- .inset(true)
- .toggle_state(selected)
- .child(Label::new(thread.summary.clone()))
- .when(added, |el| {
- el.end_slot(
- h_flex()
- .gap_1()
- .child(
- Icon::new(IconName::Check)
- .size(IconSize::Small)
- .color(Color::Success),
- )
- .child(Label::new("Added").size(LabelSize::Small)),
+pub fn render_thread_context_entry(
+ thread: &ThreadContextEntry,
+ context_store: WeakModel<ContextStore>,
+ cx: &mut WindowContext,
+) -> Div {
+ let added = context_store.upgrade().map_or(false, |ctx_store| {
+ ctx_store.read(cx).includes_thread(&thread.id).is_some()
+ });
+
+ h_flex()
+ .gap_1()
+ .w_full()
+ .child(Icon::new(IconName::MessageCircle).size(IconSize::Small))
+ .child(Label::new(thread.summary.clone()))
+ .child(div().w_full())
+ .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)),
+ )
+ })
}
@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{anyhow, bail, Result};
-use collections::{BTreeMap, HashMap};
+use collections::{BTreeMap, HashMap, HashSet};
use futures::{self, future, Future, FutureExt};
use gpui::{AppContext, AsyncAppContext, Model, ModelContext, SharedString, Task, WeakView};
use language::Buffer;
@@ -372,6 +372,23 @@ impl ContextStore {
}
}
}
+
+ pub fn file_paths(&self, cx: &AppContext) -> HashSet<PathBuf> {
+ self.context
+ .iter()
+ .filter_map(|context| match context {
+ Context::File(file) => {
+ let buffer = file.context_buffer.buffer.read(cx);
+ buffer_path_log_err(buffer).map(|p| p.to_path_buf())
+ }
+ Context::Directory(_) | Context::FetchedUrl(_) | Context::Thread(_) => None,
+ })
+ .collect()
+ }
+
+ pub fn thread_ids(&self) -> HashSet<ThreadId> {
+ self.threads.keys().cloned().collect()
+ }
}
pub enum FileInclusion {
@@ -23,7 +23,7 @@ use crate::{AssistantPanel, RemoveAllContext, ToggleContextPicker};
pub struct ContextStrip {
context_store: Model<ContextStore>,
- context_picker: View<ContextPicker>,
+ pub context_picker: View<ContextPicker>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
focus_handle: FocusHandle,
suggest_context_kind: SuggestContextKind,
@@ -126,7 +126,7 @@ impl ContextStrip {
}
Some(SuggestedContext::Thread {
- name: active_thread.summary().unwrap_or("New Thread".into()),
+ name: active_thread.summary_or_default(),
thread: weak_active_thread,
})
}
@@ -168,7 +168,13 @@ impl Render for ContextStrip {
.gap_1()
.child(
PopoverMenu::new("context-picker")
- .menu(move |_cx| Some(context_picker.clone()))
+ .menu(move |cx| {
+ context_picker.update(cx, |this, cx| {
+ this.reset_mode(cx);
+ });
+
+ Some(context_picker.clone())
+ })
.trigger(
IconButton::new("add-context", IconName::Plus)
.icon_size(IconSize::Small)
@@ -114,6 +114,11 @@ impl Thread {
self.summary.clone()
}
+ pub fn summary_or_default(&self) -> SharedString {
+ const DEFAULT: SharedString = SharedString::new_static("New Thread");
+ self.summary.clone().unwrap_or(DEFAULT)
+ }
+
pub fn set_summary(&mut self, summary: impl Into<SharedString>, cx: &mut ModelContext<Self>) {
self.summary = Some(summary.into());
cx.emit(ThreadEvent::SummaryChanged);
@@ -100,12 +100,8 @@ impl PastThread {
impl RenderOnce for PastThread {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let (id, summary) = {
- const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
let thread = self.thread.read(cx);
- (
- thread.id().clone(),
- thread.summary().unwrap_or(DEFAULT_SUMMARY),
- )
+ (thread.id().clone(), thread.summary_or_default())
};
let thread_timestamp = time_format::format_localized_timestamp(
@@ -12,19 +12,11 @@ use settings::Settings;
use std::{rc::Rc, time::Duration};
use theme::ThemeSettings;
-enum ContextMenuItem {
+pub enum ContextMenuItem {
Separator,
Header(SharedString),
Label(SharedString),
- Entry {
- toggle: Option<(IconPosition, bool)>,
- label: SharedString,
- icon: Option<IconName>,
- icon_size: IconSize,
- handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
- action: Option<Box<dyn Action>>,
- disabled: bool,
- },
+ Entry(ContextMenuEntry),
CustomEntry {
entry_render: Box<dyn Fn(&mut WindowContext) -> AnyElement>,
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
@@ -32,6 +24,86 @@ enum ContextMenuItem {
},
}
+impl ContextMenuItem {
+ pub fn custom_entry(
+ entry_render: impl Fn(&mut WindowContext) -> AnyElement + 'static,
+ handler: impl Fn(&mut WindowContext) + 'static,
+ ) -> Self {
+ Self::CustomEntry {
+ entry_render: Box::new(entry_render),
+ handler: Rc::new(move |_, cx| handler(cx)),
+ selectable: true,
+ }
+ }
+}
+
+pub struct ContextMenuEntry {
+ toggle: Option<(IconPosition, bool)>,
+ label: SharedString,
+ icon: Option<IconName>,
+ icon_size: IconSize,
+ icon_position: IconPosition,
+ handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
+ action: Option<Box<dyn Action>>,
+ disabled: bool,
+}
+
+impl ContextMenuEntry {
+ pub fn new(label: impl Into<SharedString>) -> Self {
+ ContextMenuEntry {
+ toggle: None,
+ label: label.into(),
+ icon: None,
+ icon_size: IconSize::Small,
+ icon_position: IconPosition::Start,
+ handler: Rc::new(|_, _| {}),
+ action: None,
+ disabled: false,
+ }
+ }
+
+ pub fn icon(mut self, icon: IconName) -> Self {
+ self.icon = Some(icon);
+ self
+ }
+
+ pub fn icon_position(mut self, position: IconPosition) -> Self {
+ self.icon_position = position;
+ self
+ }
+
+ pub fn icon_size(mut self, icon_size: IconSize) -> Self {
+ self.icon_size = icon_size;
+ self
+ }
+
+ pub fn toggle(mut self, toggle_position: IconPosition, toggled: bool) -> Self {
+ self.toggle = Some((toggle_position, toggled));
+ self
+ }
+
+ pub fn action(mut self, action: Option<Box<dyn Action>>) -> Self {
+ self.action = action;
+ self
+ }
+
+ pub fn handler(mut self, handler: impl Fn(&mut WindowContext) + 'static) -> Self {
+ self.handler = Rc::new(move |_, cx| handler(cx));
+ self
+ }
+
+ pub fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
+}
+
+impl From<ContextMenuEntry> for ContextMenuItem {
+ fn from(entry: ContextMenuEntry) -> Self {
+ ContextMenuItem::Entry(entry)
+ }
+}
+
pub struct ContextMenu {
items: Vec<ContextMenuItem>,
focus_handle: FocusHandle,
@@ -93,21 +165,32 @@ impl ContextMenu {
self
}
+ pub fn extend<I: Into<ContextMenuItem>>(mut self, items: impl IntoIterator<Item = I>) -> Self {
+ self.items.extend(items.into_iter().map(Into::into));
+ self
+ }
+
+ pub fn item(mut self, item: impl Into<ContextMenuItem>) -> Self {
+ self.items.push(item.into());
+ self
+ }
+
pub fn entry(
mut self,
label: impl Into<SharedString>,
action: Option<Box<dyn Action>>,
handler: impl Fn(&mut WindowContext) + 'static,
) -> Self {
- self.items.push(ContextMenuItem::Entry {
+ self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
toggle: None,
label: label.into(),
handler: Rc::new(move |_, cx| handler(cx)),
icon: None,
icon_size: IconSize::Small,
+ icon_position: IconPosition::End,
action,
disabled: false,
- });
+ }));
self
}
@@ -119,15 +202,16 @@ impl ContextMenu {
action: Option<Box<dyn Action>>,
handler: impl Fn(&mut WindowContext) + 'static,
) -> Self {
- self.items.push(ContextMenuItem::Entry {
+ self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
toggle: Some((position, toggled)),
label: label.into(),
handler: Rc::new(move |_, cx| handler(cx)),
icon: None,
icon_size: IconSize::Small,
+ icon_position: position,
action,
disabled: false,
- });
+ }));
self
}
@@ -162,7 +246,7 @@ impl ContextMenu {
}
pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
- self.items.push(ContextMenuItem::Entry {
+ self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
toggle: None,
label: label.into(),
action: Some(action.boxed_clone()),
@@ -174,9 +258,10 @@ impl ContextMenu {
cx.dispatch_action(action.boxed_clone());
}),
icon: None,
+ icon_position: IconPosition::End,
icon_size: IconSize::Small,
disabled: false,
- });
+ }));
self
}
@@ -185,7 +270,7 @@ impl ContextMenu {
label: impl Into<SharedString>,
action: Box<dyn Action>,
) -> Self {
- self.items.push(ContextMenuItem::Entry {
+ self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
toggle: None,
label: label.into(),
action: Some(action.boxed_clone()),
@@ -198,13 +283,14 @@ impl ContextMenu {
}),
icon: None,
icon_size: IconSize::Small,
+ icon_position: IconPosition::End,
disabled: true,
- });
+ }));
self
}
pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
- self.items.push(ContextMenuItem::Entry {
+ self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
toggle: None,
label: label.into(),
@@ -212,19 +298,20 @@ impl ContextMenu {
handler: Rc::new(move |_, cx| cx.dispatch_action(action.boxed_clone())),
icon: Some(IconName::ArrowUpRight),
icon_size: IconSize::XSmall,
+ icon_position: IconPosition::End,
disabled: false,
- });
+ }));
self
}
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let context = self.action_context.as_ref();
if let Some(
- ContextMenuItem::Entry {
+ ContextMenuItem::Entry(ContextMenuEntry {
handler,
disabled: false,
..
- }
+ })
| ContextMenuItem::CustomEntry { handler, .. },
) = self.selected_index.and_then(|ix| self.items.get(ix))
{
@@ -304,11 +391,11 @@ impl ContextMenu {
}
if let Some(ix) = self.items.iter().position(|item| {
- if let ContextMenuItem::Entry {
+ if let ContextMenuItem::Entry(ContextMenuEntry {
action: Some(action),
disabled: false,
..
- } = item
+ }) = item
{
action.partial_eq(dispatched)
} else {
@@ -346,7 +433,7 @@ impl ContextMenuItem {
ContextMenuItem::Header(_)
| ContextMenuItem::Separator
| ContextMenuItem::Label { .. } => false,
- ContextMenuItem::Entry { disabled, .. } => !disabled,
+ ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
}
}
@@ -356,12 +443,17 @@ impl Render for ContextMenu {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
- div().occlude().elevation_2(cx).flex().flex_row().child(
- WithRemSize::new(ui_font_size).flex().child(
+ WithRemSize::new(ui_font_size)
+ .occlude()
+ .elevation_2(cx)
+ .flex()
+ .flex_row()
+ .child(
v_flex()
.id("context-menu")
.min_w(px(200.))
.max_h(vh(0.75, cx))
+ .flex_1()
.overflow_y_scroll()
.track_focus(&self.focus_handle(cx))
.on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&menu::Cancel, cx)))
@@ -374,11 +466,11 @@ impl Render for ContextMenu {
.on_action(cx.listener(ContextMenu::cancel))
.when(!self.delayed, |mut el| {
for item in self.items.iter() {
- if let ContextMenuItem::Entry {
+ if let ContextMenuItem::Entry(ContextMenuEntry {
action: Some(action),
disabled: false,
..
- } = item
+ }) = item
{
el = el.on_boxed_action(
&**action,
@@ -388,7 +480,6 @@ impl Render for ContextMenu {
}
el
})
- .flex_none()
.child(List::new().children(self.items.iter_mut().enumerate().map(
|(ix, item)| {
match item {
@@ -403,15 +494,16 @@ impl Render for ContextMenu {
.disabled(true)
.child(Label::new(label.clone()))
.into_any_element(),
- ContextMenuItem::Entry {
+ ContextMenuItem::Entry(ContextMenuEntry {
toggle,
label,
handler,
icon,
icon_size,
+ icon_position,
action,
disabled,
- } => {
+ }) => {
let handler = handler.clone();
let menu = cx.view().downgrade();
let color = if *disabled {
@@ -422,10 +514,21 @@ impl Render for ContextMenu {
let label_element = if let Some(icon_name) = icon {
h_flex()
.gap_1()
+ .when(*icon_position == IconPosition::Start, |flex| {
+ flex.child(
+ Icon::new(*icon_name)
+ .size(*icon_size)
+ .color(color),
+ )
+ })
.child(Label::new(label.clone()).color(color))
- .child(
- Icon::new(*icon_name).size(*icon_size).color(color),
- )
+ .when(*icon_position == IconPosition::End, |flex| {
+ flex.child(
+ Icon::new(*icon_name)
+ .size(*icon_size)
+ .color(color),
+ )
+ })
.into_any_element()
} else {
Label::new(label.clone()).color(color).into_any_element()
@@ -520,7 +623,6 @@ impl Render for ContextMenu {
}
},
))),
- ),
- )
+ )
}
}
@@ -1,6 +1,7 @@
use gpui::{
div, AnyElement, Bounds, Div, DivFrameState, Element, ElementId, GlobalElementId, Hitbox,
- IntoElement, LayoutId, ParentElement, Pixels, StyleRefinement, Styled, WindowContext,
+ InteractiveElement as _, IntoElement, LayoutId, ParentElement, Pixels, StyleRefinement, Styled,
+ WindowContext,
};
/// An element that sets a particular rem size for its children.
@@ -18,6 +19,13 @@ impl WithRemSize {
rem_size: rem_size.into(),
}
}
+
+ /// Block the mouse from interacting with this element or any of its children
+ /// The fluent API equivalent to [`Interactivity::occlude_mouse`]
+ pub fn occlude(mut self) -> Self {
+ self.div = self.div.occlude();
+ self
+ }
}
impl Styled for WithRemSize {
@@ -37,7 +45,7 @@ impl Element for WithRemSize {
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
- self.div.id()
+ Element::id(&self.div)
}
fn request_layout(
@@ -1316,11 +1316,10 @@ impl Workspace {
&self.project
}
- pub fn recent_navigation_history(
+ pub fn recent_navigation_history_iter(
&self,
- limit: Option<usize>,
cx: &AppContext,
- ) -> Vec<(ProjectPath, Option<PathBuf>)> {
+ ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> {
let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
for pane in &self.panes {
@@ -1353,7 +1352,7 @@ impl Workspace {
.sorted_by_key(|(_, (_, timestamp))| *timestamp)
.map(|(project_path, (fs_path, _))| (project_path, fs_path))
.rev()
- .filter(|(history_path, abs_path)| {
+ .filter(move |(history_path, abs_path)| {
let latest_project_path_opened = abs_path
.as_ref()
.and_then(|abs_path| abs_paths_opened.get(abs_path))
@@ -1368,6 +1367,14 @@ impl Workspace {
None => true,
}
})
+ }
+
+ pub fn recent_navigation_history(
+ &self,
+ limit: Option<usize>,
+ cx: &AppContext,
+ ) -> Vec<(ProjectPath, Option<PathBuf>)> {
+ self.recent_navigation_history_iter(cx)
.take(limit.unwrap_or(usize::MAX))
.collect()
}