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()
|
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> {
|
pub fn last_error(&self) -> Option<ThreadError> {
|
||||||
self.last_error.clone()
|
self.last_error.clone()
|
||||||
}
|
}
|
||||||
|
|
|
@ -300,11 +300,12 @@ impl AssistantPanel {
|
||||||
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let focus_handle = self.focus_handle(cx);
|
let focus_handle = self.focus_handle(cx);
|
||||||
|
|
||||||
let title = if self.thread.read(cx).is_empty() {
|
let thread = self.thread.read(cx);
|
||||||
SharedString::from("New Thread")
|
|
||||||
|
let title = if thread.is_empty() {
|
||||||
|
thread.summary_or_default(cx)
|
||||||
} else {
|
} else {
|
||||||
self.thread
|
thread
|
||||||
.read(cx)
|
|
||||||
.summary(cx)
|
.summary(cx)
|
||||||
.unwrap_or_else(|| SharedString::from("Loading Summary…"))
|
.unwrap_or_else(|| SharedString::from("Loading Summary…"))
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,6 +43,24 @@ pub enum ContextKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn icon(&self) -> IconName {
|
||||||
match self {
|
match self {
|
||||||
ContextKind::File => IconName::File,
|
ContextKind::File => IconName::File,
|
||||||
|
|
|
@ -3,15 +3,17 @@ mod fetch_context_picker;
|
||||||
mod file_context_picker;
|
mod file_context_picker;
|
||||||
mod thread_context_picker;
|
mod thread_context_picker;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use editor::Editor;
|
||||||
|
use file_context_picker::render_file_context_entry;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
|
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakModel, WeakView,
|
||||||
WeakModel, WeakView,
|
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use project::ProjectPath;
|
||||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
|
||||||
use util::ResultExt;
|
use ui::{prelude::*, ContextMenu, ContextMenuEntry, ContextMenuItem};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use crate::context::ContextKind;
|
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_picker::thread_context_picker::ThreadContextPicker;
|
||||||
use crate::context_store::ContextStore;
|
use crate::context_store::ContextStore;
|
||||||
use crate::thread_store::ThreadStore;
|
use crate::thread_store::ThreadStore;
|
||||||
|
use crate::AssistantPanel;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum ConfirmBehavior {
|
pub enum ConfirmBehavior {
|
||||||
|
@ -30,7 +33,7 @@ pub enum ConfirmBehavior {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum ContextPickerMode {
|
enum ContextPickerMode {
|
||||||
Default,
|
Default(View<ContextMenu>),
|
||||||
File(View<FileContextPicker>),
|
File(View<FileContextPicker>),
|
||||||
Directory(View<DirectoryContextPicker>),
|
Directory(View<DirectoryContextPicker>),
|
||||||
Fetch(View<FetchContextPicker>),
|
Fetch(View<FetchContextPicker>),
|
||||||
|
@ -39,7 +42,10 @@ enum ContextPickerMode {
|
||||||
|
|
||||||
pub(super) struct ContextPicker {
|
pub(super) struct ContextPicker {
|
||||||
mode: ContextPickerMode,
|
mode: ContextPickerMode,
|
||||||
picker: View<Picker<ContextPickerDelegate>>,
|
workspace: WeakView<Workspace>,
|
||||||
|
context_store: WeakModel<ContextStore>,
|
||||||
|
thread_store: Option<WeakModel<ThreadStore>>,
|
||||||
|
confirm_behavior: ConfirmBehavior,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextPicker {
|
impl ContextPicker {
|
||||||
|
@ -50,53 +56,287 @@ impl ContextPicker {
|
||||||
confirm_behavior: ConfirmBehavior,
|
confirm_behavior: ConfirmBehavior,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> 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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let delegate = ContextPickerDelegate {
|
|
||||||
context_picker: cx.view().downgrade(),
|
|
||||||
workspace,
|
|
||||||
thread_store,
|
|
||||||
context_store,
|
|
||||||
confirm_behavior,
|
|
||||||
entries,
|
|
||||||
selected_ix: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let picker = cx.new_view(|cx| {
|
|
||||||
Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
|
|
||||||
});
|
|
||||||
|
|
||||||
ContextPicker {
|
ContextPicker {
|
||||||
mode: ContextPickerMode::Default,
|
mode: ContextPickerMode::Default(ContextMenu::build(cx, |menu, _cx| menu)),
|
||||||
picker,
|
workspace,
|
||||||
|
context_store,
|
||||||
|
thread_store,
|
||||||
|
confirm_behavior,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_mode(&mut self) {
|
pub fn reset_mode(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.mode = ContextPickerMode::Default;
|
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)
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 {
|
impl FocusableView for ContextPicker {
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
match &self.mode {
|
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::File(file_picker) => file_picker.focus_handle(cx),
|
||||||
ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx),
|
ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx),
|
||||||
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
||||||
|
@ -120,7 +360,7 @@ impl Render for ContextPicker {
|
||||||
.w(px(400.))
|
.w(px(400.))
|
||||||
.min_w(px(400.))
|
.min_w(px(400.))
|
||||||
.map(|parent| match &self.mode {
|
.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::File(file_picker) => parent.child(file_picker.clone()),
|
||||||
ContextPickerMode::Directory(directory_picker) => {
|
ContextPickerMode::Directory(directory_picker) => {
|
||||||
parent.child(directory_picker.clone())
|
parent.child(directory_picker.clone())
|
||||||
|
@ -130,140 +370,10 @@ impl Render for ContextPicker {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
enum RecentEntry {
|
||||||
#[derive(Clone)]
|
File {
|
||||||
struct ContextPickerEntry {
|
project_path: ProjectPath,
|
||||||
name: SharedString,
|
path_prefix: Arc<str>,
|
||||||
kind: ContextKind,
|
},
|
||||||
icon: IconName,
|
Thread(ThreadContextEntry),
|
||||||
}
|
|
||||||
|
|
||||||
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()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -222,7 +222,7 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
self.context_picker
|
self.context_picker
|
||||||
.update(cx, |this, cx| {
|
.update(cx, |this, cx| {
|
||||||
this.reset_mode();
|
this.reset_mode(cx);
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
|
@ -225,7 +225,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
self.context_picker
|
self.context_picker
|
||||||
.update(cx, |this, cx| {
|
.update(cx, |this, cx| {
|
||||||
this.reset_mode();
|
this.reset_mode(cx);
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
|
@ -4,7 +4,9 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use fuzzy::PathMatch;
|
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 picker::{Picker, PickerDelegate};
|
||||||
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
||||||
use ui::{prelude::*, ListItem, Tooltip};
|
use ui::{prelude::*, ListItem, Tooltip};
|
||||||
|
@ -238,7 +240,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
self.context_picker
|
self.context_picker
|
||||||
.update(cx, |this, cx| {
|
.update(cx, |this, cx| {
|
||||||
this.reset_mode();
|
this.reset_mode(cx);
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
@ -252,82 +254,97 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
let path_match = &self.matches[ix];
|
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(
|
Some(
|
||||||
ListItem::new(ix)
|
ListItem::new(ix)
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
.child(
|
.child(render_file_context_entry(
|
||||||
h_flex()
|
ElementId::NamedInteger("file-ctx-picker".into(), ix),
|
||||||
.gap_2()
|
&path_match.path,
|
||||||
.child(file_icon.size(IconSize::Small))
|
&path_match.path_prefix,
|
||||||
.child(Label::new(file_name))
|
self.context_store.clone(),
|
||||||
.children(directory.map(|directory| {
|
cx,
|
||||||
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))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 ui::{prelude::*, ListItem};
|
||||||
|
|
||||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||||
use crate::context_store;
|
use crate::context_store::{self, ContextStore};
|
||||||
use crate::thread::ThreadId;
|
use crate::thread::ThreadId;
|
||||||
use crate::thread_store::ThreadStore;
|
use crate::thread_store::ThreadStore;
|
||||||
|
|
||||||
|
@ -47,9 +47,9 @@ impl Render for ThreadContextPicker {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct ThreadContextEntry {
|
pub struct ThreadContextEntry {
|
||||||
id: ThreadId,
|
pub id: ThreadId,
|
||||||
summary: SharedString,
|
pub summary: SharedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ThreadContextPickerDelegate {
|
pub struct ThreadContextPickerDelegate {
|
||||||
|
@ -103,10 +103,8 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||||
this.threads(cx)
|
this.threads(cx)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|thread| {
|
.map(|thread| {
|
||||||
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
|
|
||||||
|
|
||||||
let id = thread.read(cx).id().clone();
|
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 }
|
ThreadContextEntry { id, summary }
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
@ -179,7 +177,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
self.context_picker
|
self.context_picker
|
||||||
.update(cx, |this, cx| {
|
.update(cx, |this, cx| {
|
||||||
this.reset_mode();
|
this.reset_mode(cx);
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
@ -193,27 +191,37 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
let thread = &self.matches[ix];
|
let thread = &self.matches[ix];
|
||||||
|
|
||||||
let added = self.context_store.upgrade().map_or(false, |context_store| {
|
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||||
context_store.read(cx).includes_thread(&thread.id).is_some()
|
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 std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use collections::{BTreeMap, HashMap};
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
use futures::{self, future, Future, FutureExt};
|
use futures::{self, future, Future, FutureExt};
|
||||||
use gpui::{AppContext, AsyncAppContext, Model, ModelContext, SharedString, Task, WeakView};
|
use gpui::{AppContext, AsyncAppContext, Model, ModelContext, SharedString, Task, WeakView};
|
||||||
use language::Buffer;
|
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 {
|
pub enum FileInclusion {
|
||||||
|
|
|
@ -23,7 +23,7 @@ use crate::{AssistantPanel, RemoveAllContext, ToggleContextPicker};
|
||||||
|
|
||||||
pub struct ContextStrip {
|
pub struct ContextStrip {
|
||||||
context_store: Model<ContextStore>,
|
context_store: Model<ContextStore>,
|
||||||
context_picker: View<ContextPicker>,
|
pub context_picker: View<ContextPicker>,
|
||||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
suggest_context_kind: SuggestContextKind,
|
suggest_context_kind: SuggestContextKind,
|
||||||
|
@ -126,7 +126,7 @@ impl ContextStrip {
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(SuggestedContext::Thread {
|
Some(SuggestedContext::Thread {
|
||||||
name: active_thread.summary().unwrap_or("New Thread".into()),
|
name: active_thread.summary_or_default(),
|
||||||
thread: weak_active_thread,
|
thread: weak_active_thread,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -168,7 +168,13 @@ impl Render for ContextStrip {
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
PopoverMenu::new("context-picker")
|
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(
|
.trigger(
|
||||||
IconButton::new("add-context", IconName::Plus)
|
IconButton::new("add-context", IconName::Plus)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
|
|
|
@ -114,6 +114,11 @@ impl Thread {
|
||||||
self.summary.clone()
|
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>) {
|
pub fn set_summary(&mut self, summary: impl Into<SharedString>, cx: &mut ModelContext<Self>) {
|
||||||
self.summary = Some(summary.into());
|
self.summary = Some(summary.into());
|
||||||
cx.emit(ThreadEvent::SummaryChanged);
|
cx.emit(ThreadEvent::SummaryChanged);
|
||||||
|
|
|
@ -100,12 +100,8 @@ impl PastThread {
|
||||||
impl RenderOnce for PastThread {
|
impl RenderOnce for PastThread {
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
let (id, summary) = {
|
let (id, summary) = {
|
||||||
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
|
|
||||||
let thread = self.thread.read(cx);
|
let thread = self.thread.read(cx);
|
||||||
(
|
(thread.id().clone(), thread.summary_or_default())
|
||||||
thread.id().clone(),
|
|
||||||
thread.summary().unwrap_or(DEFAULT_SUMMARY),
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let thread_timestamp = time_format::format_localized_timestamp(
|
let thread_timestamp = time_format::format_localized_timestamp(
|
||||||
|
|
|
@ -12,19 +12,11 @@ use settings::Settings;
|
||||||
use std::{rc::Rc, time::Duration};
|
use std::{rc::Rc, time::Duration};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
|
|
||||||
enum ContextMenuItem {
|
pub enum ContextMenuItem {
|
||||||
Separator,
|
Separator,
|
||||||
Header(SharedString),
|
Header(SharedString),
|
||||||
Label(SharedString),
|
Label(SharedString),
|
||||||
Entry {
|
Entry(ContextMenuEntry),
|
||||||
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,
|
|
||||||
},
|
|
||||||
CustomEntry {
|
CustomEntry {
|
||||||
entry_render: Box<dyn Fn(&mut WindowContext) -> AnyElement>,
|
entry_render: Box<dyn Fn(&mut WindowContext) -> AnyElement>,
|
||||||
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
|
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 {
|
pub struct ContextMenu {
|
||||||
items: Vec<ContextMenuItem>,
|
items: Vec<ContextMenuItem>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
|
@ -93,21 +165,32 @@ impl ContextMenu {
|
||||||
self
|
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(
|
pub fn entry(
|
||||||
mut self,
|
mut self,
|
||||||
label: impl Into<SharedString>,
|
label: impl Into<SharedString>,
|
||||||
action: Option<Box<dyn Action>>,
|
action: Option<Box<dyn Action>>,
|
||||||
handler: impl Fn(&mut WindowContext) + 'static,
|
handler: impl Fn(&mut WindowContext) + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.items.push(ContextMenuItem::Entry {
|
self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
|
||||||
toggle: None,
|
toggle: None,
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
handler: Rc::new(move |_, cx| handler(cx)),
|
handler: Rc::new(move |_, cx| handler(cx)),
|
||||||
icon: None,
|
icon: None,
|
||||||
icon_size: IconSize::Small,
|
icon_size: IconSize::Small,
|
||||||
|
icon_position: IconPosition::End,
|
||||||
action,
|
action,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
});
|
}));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,15 +202,16 @@ impl ContextMenu {
|
||||||
action: Option<Box<dyn Action>>,
|
action: Option<Box<dyn Action>>,
|
||||||
handler: impl Fn(&mut WindowContext) + 'static,
|
handler: impl Fn(&mut WindowContext) + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.items.push(ContextMenuItem::Entry {
|
self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
|
||||||
toggle: Some((position, toggled)),
|
toggle: Some((position, toggled)),
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
handler: Rc::new(move |_, cx| handler(cx)),
|
handler: Rc::new(move |_, cx| handler(cx)),
|
||||||
icon: None,
|
icon: None,
|
||||||
icon_size: IconSize::Small,
|
icon_size: IconSize::Small,
|
||||||
|
icon_position: position,
|
||||||
action,
|
action,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
});
|
}));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,7 +246,7 @@ impl ContextMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
|
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,
|
toggle: None,
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
action: Some(action.boxed_clone()),
|
action: Some(action.boxed_clone()),
|
||||||
|
@ -174,9 +258,10 @@ impl ContextMenu {
|
||||||
cx.dispatch_action(action.boxed_clone());
|
cx.dispatch_action(action.boxed_clone());
|
||||||
}),
|
}),
|
||||||
icon: None,
|
icon: None,
|
||||||
|
icon_position: IconPosition::End,
|
||||||
icon_size: IconSize::Small,
|
icon_size: IconSize::Small,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
});
|
}));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +270,7 @@ impl ContextMenu {
|
||||||
label: impl Into<SharedString>,
|
label: impl Into<SharedString>,
|
||||||
action: Box<dyn Action>,
|
action: Box<dyn Action>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.items.push(ContextMenuItem::Entry {
|
self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
|
||||||
toggle: None,
|
toggle: None,
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
action: Some(action.boxed_clone()),
|
action: Some(action.boxed_clone()),
|
||||||
|
@ -198,13 +283,14 @@ impl ContextMenu {
|
||||||
}),
|
}),
|
||||||
icon: None,
|
icon: None,
|
||||||
icon_size: IconSize::Small,
|
icon_size: IconSize::Small,
|
||||||
|
icon_position: IconPosition::End,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
});
|
}));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> 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,
|
toggle: None,
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
|
|
||||||
|
@ -212,19 +298,20 @@ impl ContextMenu {
|
||||||
handler: Rc::new(move |_, cx| cx.dispatch_action(action.boxed_clone())),
|
handler: Rc::new(move |_, cx| cx.dispatch_action(action.boxed_clone())),
|
||||||
icon: Some(IconName::ArrowUpRight),
|
icon: Some(IconName::ArrowUpRight),
|
||||||
icon_size: IconSize::XSmall,
|
icon_size: IconSize::XSmall,
|
||||||
|
icon_position: IconPosition::End,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
});
|
}));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||||
let context = self.action_context.as_ref();
|
let context = self.action_context.as_ref();
|
||||||
if let Some(
|
if let Some(
|
||||||
ContextMenuItem::Entry {
|
ContextMenuItem::Entry(ContextMenuEntry {
|
||||||
handler,
|
handler,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
..
|
..
|
||||||
}
|
})
|
||||||
| ContextMenuItem::CustomEntry { handler, .. },
|
| ContextMenuItem::CustomEntry { handler, .. },
|
||||||
) = self.selected_index.and_then(|ix| self.items.get(ix))
|
) = 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 Some(ix) = self.items.iter().position(|item| {
|
||||||
if let ContextMenuItem::Entry {
|
if let ContextMenuItem::Entry(ContextMenuEntry {
|
||||||
action: Some(action),
|
action: Some(action),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
..
|
..
|
||||||
} = item
|
}) = item
|
||||||
{
|
{
|
||||||
action.partial_eq(dispatched)
|
action.partial_eq(dispatched)
|
||||||
} else {
|
} else {
|
||||||
|
@ -346,7 +433,7 @@ impl ContextMenuItem {
|
||||||
ContextMenuItem::Header(_)
|
ContextMenuItem::Header(_)
|
||||||
| ContextMenuItem::Separator
|
| ContextMenuItem::Separator
|
||||||
| ContextMenuItem::Label { .. } => false,
|
| ContextMenuItem::Label { .. } => false,
|
||||||
ContextMenuItem::Entry { disabled, .. } => !disabled,
|
ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
|
||||||
ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
|
ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -356,12 +443,17 @@ impl Render for ContextMenu {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
|
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)
|
||||||
WithRemSize::new(ui_font_size).flex().child(
|
.occlude()
|
||||||
|
.elevation_2(cx)
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("context-menu")
|
.id("context-menu")
|
||||||
.min_w(px(200.))
|
.min_w(px(200.))
|
||||||
.max_h(vh(0.75, cx))
|
.max_h(vh(0.75, cx))
|
||||||
|
.flex_1()
|
||||||
.overflow_y_scroll()
|
.overflow_y_scroll()
|
||||||
.track_focus(&self.focus_handle(cx))
|
.track_focus(&self.focus_handle(cx))
|
||||||
.on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&menu::Cancel, 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))
|
.on_action(cx.listener(ContextMenu::cancel))
|
||||||
.when(!self.delayed, |mut el| {
|
.when(!self.delayed, |mut el| {
|
||||||
for item in self.items.iter() {
|
for item in self.items.iter() {
|
||||||
if let ContextMenuItem::Entry {
|
if let ContextMenuItem::Entry(ContextMenuEntry {
|
||||||
action: Some(action),
|
action: Some(action),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
..
|
..
|
||||||
} = item
|
}) = item
|
||||||
{
|
{
|
||||||
el = el.on_boxed_action(
|
el = el.on_boxed_action(
|
||||||
&**action,
|
&**action,
|
||||||
|
@ -388,7 +480,6 @@ impl Render for ContextMenu {
|
||||||
}
|
}
|
||||||
el
|
el
|
||||||
})
|
})
|
||||||
.flex_none()
|
|
||||||
.child(List::new().children(self.items.iter_mut().enumerate().map(
|
.child(List::new().children(self.items.iter_mut().enumerate().map(
|
||||||
|(ix, item)| {
|
|(ix, item)| {
|
||||||
match item {
|
match item {
|
||||||
|
@ -403,15 +494,16 @@ impl Render for ContextMenu {
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
.child(Label::new(label.clone()))
|
.child(Label::new(label.clone()))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
ContextMenuItem::Entry {
|
ContextMenuItem::Entry(ContextMenuEntry {
|
||||||
toggle,
|
toggle,
|
||||||
label,
|
label,
|
||||||
handler,
|
handler,
|
||||||
icon,
|
icon,
|
||||||
icon_size,
|
icon_size,
|
||||||
|
icon_position,
|
||||||
action,
|
action,
|
||||||
disabled,
|
disabled,
|
||||||
} => {
|
}) => {
|
||||||
let handler = handler.clone();
|
let handler = handler.clone();
|
||||||
let menu = cx.view().downgrade();
|
let menu = cx.view().downgrade();
|
||||||
let color = if *disabled {
|
let color = if *disabled {
|
||||||
|
@ -422,10 +514,21 @@ impl Render for ContextMenu {
|
||||||
let label_element = if let Some(icon_name) = icon {
|
let label_element = if let Some(icon_name) = icon {
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.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(Label::new(label.clone()).color(color))
|
||||||
.child(
|
.when(*icon_position == IconPosition::End, |flex| {
|
||||||
Icon::new(*icon_name).size(*icon_size).color(color),
|
flex.child(
|
||||||
)
|
Icon::new(*icon_name)
|
||||||
|
.size(*icon_size)
|
||||||
|
.color(color),
|
||||||
|
)
|
||||||
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
Label::new(label.clone()).color(color).into_any_element()
|
Label::new(label.clone()).color(color).into_any_element()
|
||||||
|
@ -520,7 +623,6 @@ impl Render for ContextMenu {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
))),
|
))),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, AnyElement, Bounds, Div, DivFrameState, Element, ElementId, GlobalElementId, Hitbox,
|
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.
|
/// An element that sets a particular rem size for its children.
|
||||||
|
@ -18,6 +19,13 @@ impl WithRemSize {
|
||||||
rem_size: rem_size.into(),
|
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 {
|
impl Styled for WithRemSize {
|
||||||
|
@ -37,7 +45,7 @@ impl Element for WithRemSize {
|
||||||
type PrepaintState = Option<Hitbox>;
|
type PrepaintState = Option<Hitbox>;
|
||||||
|
|
||||||
fn id(&self) -> Option<ElementId> {
|
fn id(&self) -> Option<ElementId> {
|
||||||
self.div.id()
|
Element::id(&self.div)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_layout(
|
fn request_layout(
|
||||||
|
|
|
@ -1316,11 +1316,10 @@ impl Workspace {
|
||||||
&self.project
|
&self.project
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recent_navigation_history(
|
pub fn recent_navigation_history_iter(
|
||||||
&self,
|
&self,
|
||||||
limit: Option<usize>,
|
|
||||||
cx: &AppContext,
|
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 abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
|
||||||
let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
|
let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
|
||||||
for pane in &self.panes {
|
for pane in &self.panes {
|
||||||
|
@ -1353,7 +1352,7 @@ impl Workspace {
|
||||||
.sorted_by_key(|(_, (_, timestamp))| *timestamp)
|
.sorted_by_key(|(_, (_, timestamp))| *timestamp)
|
||||||
.map(|(project_path, (fs_path, _))| (project_path, fs_path))
|
.map(|(project_path, (fs_path, _))| (project_path, fs_path))
|
||||||
.rev()
|
.rev()
|
||||||
.filter(|(history_path, abs_path)| {
|
.filter(move |(history_path, abs_path)| {
|
||||||
let latest_project_path_opened = abs_path
|
let latest_project_path_opened = abs_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|abs_path| abs_paths_opened.get(abs_path))
|
.and_then(|abs_path| abs_paths_opened.get(abs_path))
|
||||||
|
@ -1368,6 +1367,14 @@ impl Workspace {
|
||||||
None => true,
|
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))
|
.take(limit.unwrap_or(usize::MAX))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue