assistant2: Suggest recent files and threads as context (#22959)
The context picker will now display up to 6 recent files/threads to add as a context: <img src="https://github.com/user-attachments/assets/80c87bf9-70ad-4e81-ba24-7a624378b991" width=400> Note: We decided to use a `ContextMenu` instead of `Picker` for the initial one since the latter didn't quite fit the design for the "Recent" section. Release Notes: - N/A --------- Co-authored-by: Danilo <danilo@zed.dev> Co-authored-by: Piotr <piotr@zed.dev> Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
parent
49198a7961
commit
a267911e83
15 changed files with 649 additions and 350 deletions
|
@ -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,
|
||||
});
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "Fetch".into(),
|
||||
kind: ContextKind::FetchedUrl,
|
||||
icon: IconName::Globe,
|
||||
});
|
||||
|
||||
if thread_store.is_some() {
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "Thread".into(),
|
||||
kind: ContextKind::Thread,
|
||||
icon: IconName::MessageCircle,
|
||||
});
|
||||
ContextPicker {
|
||||
mode: ContextPickerMode::Default(ContextMenu::build(cx, |menu, _cx| menu)),
|
||||
workspace,
|
||||
context_store,
|
||||
thread_store,
|
||||
confirm_behavior,
|
||||
}
|
||||
}
|
||||
|
||||
let delegate = ContextPickerDelegate {
|
||||
context_picker: cx.view().downgrade(),
|
||||
workspace,
|
||||
thread_store,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
entries,
|
||||
selected_ix: 0,
|
||||
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 picker = cx.new_view(|cx| {
|
||||
Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
|
||||
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)
|
||||
});
|
||||
|
||||
ContextPicker {
|
||||
mode: ContextPickerMode::Default,
|
||||
picker,
|
||||
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();
|
||||
}
|
||||
|
||||
pub fn reset_mode(&mut self) {
|
||||
self.mode = ContextPickerMode::Default;
|
||||
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);
|
||||
});
|
||||
|
||||
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 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;
|
||||
};
|
||||
|
||||
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(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
recent
|
||||
}
|
||||
|
||||
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,23 +254,41 @@ 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)
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.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_match
|
||||
.path
|
||||
let file_name = 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(""))
|
||||
{
|
||||
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('/');
|
||||
}
|
||||
|
@ -276,24 +296,22 @@ impl PickerDelegate for FileContextPickerDelegate {
|
|||
(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 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_match.path.clone(), cx)
|
||||
let file_icon = FileIcons::get_icon(&path, cx)
|
||||
.map(Icon::from_path)
|
||||
.unwrap_or_else(|| Icon::new(IconName::File));
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
h_flex()
|
||||
.id(id)
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.child(file_icon.size(IconSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(file_icon.size(IconSize::Small))
|
||||
.child(Label::new(file_name))
|
||||
.children(directory.map(|directory| {
|
||||
Label::new(directory)
|
||||
|
@ -301,8 +319,9 @@ impl PickerDelegate for FileContextPickerDelegate {
|
|||
.color(Color::Muted)
|
||||
})),
|
||||
)
|
||||
.child(div().w_full())
|
||||
.when_some(added, |el, added| match added {
|
||||
FileInclusion::Direct(_) => el.end_slot(
|
||||
FileInclusion::Direct(_) => el.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
|
@ -315,7 +334,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
|||
FileInclusion::InDirectory(dir_name) => {
|
||||
let dir_name = dir_name.to_string_lossy().into_owned();
|
||||
|
||||
el.end_slot(
|
||||
el.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
|
@ -327,7 +346,5 @@ impl PickerDelegate for FileContextPickerDelegate {
|
|||
)
|
||||
.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,17 +191,29 @@ 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),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
});
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
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.end_slot(
|
||||
el.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
|
@ -213,7 +223,5 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
|||
)
|
||||
.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()
|
||||
.child(Label::new(label.clone()).color(color))
|
||||
.child(
|
||||
Icon::new(*icon_name).size(*icon_size).color(color),
|
||||
.when(*icon_position == IconPosition::Start, |flex| {
|
||||
flex.child(
|
||||
Icon::new(*icon_name)
|
||||
.size(*icon_size)
|
||||
.color(color),
|
||||
)
|
||||
})
|
||||
.child(Label::new(label.clone()).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()
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue