acp: Fix history search (#36734)

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2025-08-21 23:57:30 -06:00 committed by Joseph T. Lyons
parent e5588fc9ea
commit 51d678d33b
5 changed files with 228 additions and 311 deletions

View file

@ -1406,10 +1406,9 @@ mod tests {
history: &Entity<HistoryStore>, history: &Entity<HistoryStore>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> Vec<(HistoryEntryId, String)> { ) -> Vec<(HistoryEntryId, String)> {
history.read_with(cx, |history, cx| { history.read_with(cx, |history, _| {
history history
.entries(cx) .entries()
.iter()
.map(|e| (e.id(), e.title().to_string())) .map(|e| (e.id(), e.title().to_string()))
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) })

View file

@ -86,6 +86,7 @@ enum SerializedRecentOpen {
pub struct HistoryStore { pub struct HistoryStore {
threads: Vec<DbThreadMetadata>, threads: Vec<DbThreadMetadata>,
entries: Vec<HistoryEntry>,
context_store: Entity<assistant_context::ContextStore>, context_store: Entity<assistant_context::ContextStore>,
recently_opened_entries: VecDeque<HistoryEntryId>, recently_opened_entries: VecDeque<HistoryEntryId>,
_subscriptions: Vec<gpui::Subscription>, _subscriptions: Vec<gpui::Subscription>,
@ -97,7 +98,7 @@ impl HistoryStore {
context_store: Entity<assistant_context::ContextStore>, context_store: Entity<assistant_context::ContextStore>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())]; let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))];
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await; let entries = Self::load_recently_opened_entries(cx).await;
@ -116,6 +117,7 @@ impl HistoryStore {
context_store, context_store,
recently_opened_entries: VecDeque::default(), recently_opened_entries: VecDeque::default(),
threads: Vec::default(), threads: Vec::default(),
entries: Vec::default(),
_subscriptions: subscriptions, _subscriptions: subscriptions,
_save_recently_opened_entries_task: Task::ready(()), _save_recently_opened_entries_task: Task::ready(()),
} }
@ -181,20 +183,18 @@ impl HistoryStore {
} }
} }
this.threads = threads; this.threads = threads;
cx.notify(); this.update_entries(cx);
}) })
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
pub fn entries(&self, cx: &App) -> Vec<HistoryEntry> { fn update_entries(&mut self, cx: &mut Context<Self>) {
let mut history_entries = Vec::new();
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return history_entries; return;
} }
let mut history_entries = Vec::new();
history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread)); history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread));
history_entries.extend( history_entries.extend(
self.context_store self.context_store
@ -205,17 +205,12 @@ impl HistoryStore {
); );
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at())); history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
history_entries self.entries = history_entries;
cx.notify()
} }
pub fn is_empty(&self, cx: &App) -> bool { pub fn is_empty(&self, _cx: &App) -> bool {
self.threads.is_empty() self.entries.is_empty()
&& self
.context_store
.read(cx)
.unordered_contexts()
.next()
.is_none()
} }
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> { pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
@ -356,7 +351,7 @@ impl HistoryStore {
self.save_recently_opened_entries(cx); self.save_recently_opened_entries(cx);
} }
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> { pub fn entries(&self) -> impl Iterator<Item = HistoryEntry> {
self.entries(cx).into_iter().take(limit).collect() self.entries.iter().cloned()
} }
} }

View file

@ -805,7 +805,7 @@ pub(crate) fn search_threads(
history_store: &Entity<HistoryStore>, history_store: &Entity<HistoryStore>,
cx: &mut App, cx: &mut App,
) -> Task<Vec<HistoryEntry>> { ) -> Task<Vec<HistoryEntry>> {
let threads = history_store.read(cx).entries(cx); let threads = history_store.read(cx).entries().collect();
if query.is_empty() { if query.is_empty() {
return Task::ready(threads); return Task::ready(threads);
} }

View file

@ -3,18 +3,18 @@ use crate::{AgentPanel, RemoveSelectedThread};
use agent2::{HistoryEntry, HistoryStore}; use agent2::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent}; use editor::{Editor, EditorEvent};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::StringMatchCandidate;
use gpui::{ use gpui::{
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
UniformListScrollHandle, WeakEntity, Window, uniform_list, UniformListScrollHandle, WeakEntity, Window, uniform_list,
}; };
use std::{fmt::Display, ops::Range, sync::Arc}; use std::{fmt::Display, ops::Range};
use text::Bias;
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use ui::{ use ui::{
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
Tooltip, prelude::*, Tooltip, prelude::*,
}; };
use util::ResultExt;
pub struct AcpThreadHistory { pub struct AcpThreadHistory {
pub(crate) history_store: Entity<HistoryStore>, pub(crate) history_store: Entity<HistoryStore>,
@ -22,38 +22,38 @@ pub struct AcpThreadHistory {
selected_index: usize, selected_index: usize,
hovered_index: Option<usize>, hovered_index: Option<usize>,
search_editor: Entity<Editor>, search_editor: Entity<Editor>,
all_entries: Arc<Vec<HistoryEntry>>, search_query: SharedString,
// When the search is empty, we display date separators between history entries
// This vector contains an enum of either a separator or an actual entry visible_items: Vec<ListItemType>,
separated_items: Vec<ListItemType>,
// Maps entry indexes to list item indexes
separated_item_indexes: Vec<u32>,
_separated_items_task: Option<Task<()>>,
search_state: SearchState,
scrollbar_visibility: bool, scrollbar_visibility: bool,
scrollbar_state: ScrollbarState, scrollbar_state: ScrollbarState,
local_timezone: UtcOffset, local_timezone: UtcOffset,
_subscriptions: Vec<gpui::Subscription>,
}
enum SearchState { _update_task: Task<()>,
Empty, _subscriptions: Vec<gpui::Subscription>,
Searching {
query: SharedString,
_task: Task<()>,
},
Searched {
query: SharedString,
matches: Vec<StringMatch>,
},
} }
enum ListItemType { enum ListItemType {
BucketSeparator(TimeBucket), BucketSeparator(TimeBucket),
Entry { Entry {
index: usize, entry: HistoryEntry,
format: EntryTimeFormat, format: EntryTimeFormat,
}, },
SearchResult {
entry: HistoryEntry,
positions: Vec<usize>,
},
}
impl ListItemType {
fn history_entry(&self) -> Option<&HistoryEntry> {
match self {
ListItemType::Entry { entry, .. } => Some(entry),
ListItemType::SearchResult { entry, .. } => Some(entry),
_ => None,
}
}
} }
pub enum ThreadHistoryEvent { pub enum ThreadHistoryEvent {
@ -78,12 +78,15 @@ impl AcpThreadHistory {
cx.subscribe(&search_editor, |this, search_editor, event, cx| { cx.subscribe(&search_editor, |this, search_editor, event, cx| {
if let EditorEvent::BufferEdited = event { if let EditorEvent::BufferEdited = event {
let query = search_editor.read(cx).text(cx); let query = search_editor.read(cx).text(cx);
this.search(query.into(), cx); if this.search_query != query {
this.search_query = query.into();
this.update_visible_items(false, cx);
}
} }
}); });
let history_store_subscription = cx.observe(&history_store, |this, _, cx| { let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
this.update_all_entries(cx); this.update_visible_items(true, cx);
}); });
let scroll_handle = UniformListScrollHandle::default(); let scroll_handle = UniformListScrollHandle::default();
@ -94,10 +97,7 @@ impl AcpThreadHistory {
scroll_handle, scroll_handle,
selected_index: 0, selected_index: 0,
hovered_index: None, hovered_index: None,
search_state: SearchState::Empty, visible_items: Default::default(),
all_entries: Default::default(),
separated_items: Default::default(),
separated_item_indexes: Default::default(),
search_editor, search_editor,
scrollbar_visibility: true, scrollbar_visibility: true,
scrollbar_state, scrollbar_state,
@ -105,29 +105,61 @@ impl AcpThreadHistory {
chrono::Local::now().offset().local_minus_utc(), chrono::Local::now().offset().local_minus_utc(),
) )
.unwrap(), .unwrap(),
search_query: SharedString::default(),
_subscriptions: vec![search_editor_subscription, history_store_subscription], _subscriptions: vec![search_editor_subscription, history_store_subscription],
_separated_items_task: None, _update_task: Task::ready(()),
}; };
this.update_all_entries(cx); this.update_visible_items(false, cx);
this this
} }
fn update_all_entries(&mut self, cx: &mut Context<Self>) { fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
let new_entries: Arc<Vec<HistoryEntry>> = self let entries = self
.history_store .history_store
.update(cx, |store, cx| store.entries(cx)) .update(cx, |store, _| store.entries().collect());
.into(); let new_list_items = if self.search_query.is_empty() {
self.add_list_separators(entries, cx)
} else {
self.filter_search_results(entries, cx)
};
let selected_history_entry = if preserve_selected_item {
self.selected_history_entry().cloned()
} else {
None
};
self._separated_items_task.take(); self._update_task = cx.spawn(async move |this, cx| {
let new_visible_items = new_list_items.await;
this.update(cx, |this, cx| {
let new_selected_index = if let Some(history_entry) = selected_history_entry {
let history_entry_id = history_entry.id();
new_visible_items
.iter()
.position(|visible_entry| {
visible_entry
.history_entry()
.is_some_and(|entry| entry.id() == history_entry_id)
})
.unwrap_or(0)
} else {
0
};
let mut items = Vec::with_capacity(new_entries.len() + 1); this.visible_items = new_visible_items;
let mut indexes = Vec::with_capacity(new_entries.len() + 1); this.set_selected_index(new_selected_index, Bias::Right, cx);
cx.notify();
})
.ok();
});
}
let bg_task = cx.background_spawn(async move { fn add_list_separators(&self, entries: Vec<HistoryEntry>, cx: &App) -> Task<Vec<ListItemType>> {
cx.background_spawn(async move {
let mut items = Vec::with_capacity(entries.len() + 1);
let mut bucket = None; let mut bucket = None;
let today = Local::now().naive_local().date(); let today = Local::now().naive_local().date();
for (index, entry) in new_entries.iter().enumerate() { for entry in entries.into_iter() {
let entry_date = entry let entry_date = entry
.updated_at() .updated_at()
.with_timezone(&Local) .with_timezone(&Local)
@ -140,75 +172,33 @@ impl AcpThreadHistory {
items.push(ListItemType::BucketSeparator(entry_bucket)); items.push(ListItemType::BucketSeparator(entry_bucket));
} }
indexes.push(items.len() as u32);
items.push(ListItemType::Entry { items.push(ListItemType::Entry {
index, entry,
format: entry_bucket.into(), format: entry_bucket.into(),
}); });
} }
(new_entries, items, indexes) items
}); })
let task = cx.spawn(async move |this, cx| {
let (new_entries, items, indexes) = bg_task.await;
this.update(cx, |this, cx| {
let previously_selected_entry =
this.all_entries.get(this.selected_index).map(|e| e.id());
this.all_entries = new_entries;
this.separated_items = items;
this.separated_item_indexes = indexes;
match &this.search_state {
SearchState::Empty => {
if this.selected_index >= this.all_entries.len() {
this.set_selected_entry_index(
this.all_entries.len().saturating_sub(1),
cx,
);
} else if let Some(prev_id) = previously_selected_entry
&& let Some(new_ix) = this
.all_entries
.iter()
.position(|probe| probe.id() == prev_id)
{
this.set_selected_entry_index(new_ix, cx);
}
}
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
this.search(query.clone(), cx);
}
}
cx.notify();
})
.log_err();
});
self._separated_items_task = Some(task);
} }
fn search(&mut self, query: SharedString, cx: &mut Context<Self>) { fn filter_search_results(
if query.is_empty() { &self,
self.search_state = SearchState::Empty; entries: Vec<HistoryEntry>,
cx.notify(); cx: &App,
return; ) -> Task<Vec<ListItemType>> {
} let query = self.search_query.clone();
cx.background_spawn({
let all_entries = self.all_entries.clone();
let fuzzy_search_task = cx.background_spawn({
let query = query.clone();
let executor = cx.background_executor().clone(); let executor = cx.background_executor().clone();
async move { async move {
let mut candidates = Vec::with_capacity(all_entries.len()); let mut candidates = Vec::with_capacity(entries.len());
for (idx, entry) in all_entries.iter().enumerate() { for (idx, entry) in entries.iter().enumerate() {
candidates.push(StringMatchCandidate::new(idx, entry.title())); candidates.push(StringMatchCandidate::new(idx, entry.title()));
} }
const MAX_MATCHES: usize = 100; const MAX_MATCHES: usize = 100;
fuzzy::match_strings( let matches = fuzzy::match_strings(
&candidates, &candidates,
&query, &query,
false, false,
@ -217,74 +207,61 @@ impl AcpThreadHistory {
&Default::default(), &Default::default(),
executor, executor,
) )
.await .await;
matches
.into_iter()
.map(|search_match| ListItemType::SearchResult {
entry: entries[search_match.candidate_id].clone(),
positions: search_match.positions,
})
.collect()
} }
}); })
let task = cx.spawn({
let query = query.clone();
async move |this, cx| {
let matches = fuzzy_search_task.await;
this.update(cx, |this, cx| {
let SearchState::Searching {
query: current_query,
_task,
} = &this.search_state
else {
return;
};
if &query == current_query {
this.search_state = SearchState::Searched {
query: query.clone(),
matches,
};
this.set_selected_entry_index(0, cx);
cx.notify();
};
})
.log_err();
}
});
self.search_state = SearchState::Searching { query, _task: task };
cx.notify();
}
fn matched_count(&self) -> usize {
match &self.search_state {
SearchState::Empty => self.all_entries.len(),
SearchState::Searching { .. } => 0,
SearchState::Searched { matches, .. } => matches.len(),
}
}
fn list_item_count(&self) -> usize {
match &self.search_state {
SearchState::Empty => self.separated_items.len(),
SearchState::Searching { .. } => 0,
SearchState::Searched { matches, .. } => matches.len(),
}
} }
fn search_produced_no_matches(&self) -> bool { fn search_produced_no_matches(&self) -> bool {
match &self.search_state { self.visible_items.is_empty() && !self.search_query.is_empty()
SearchState::Empty => false,
SearchState::Searching { .. } => false,
SearchState::Searched { matches, .. } => matches.is_empty(),
}
} }
fn get_match(&self, ix: usize) -> Option<&HistoryEntry> { fn selected_history_entry(&self) -> Option<&HistoryEntry> {
match &self.search_state { self.get_history_entry(self.selected_index)
SearchState::Empty => self.all_entries.get(ix), }
SearchState::Searching { .. } => None,
SearchState::Searched { matches, .. } => matches fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> {
.get(ix) self.visible_items.get(visible_items_ix)?.history_entry()
.and_then(|m| self.all_entries.get(m.candidate_id)), }
fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
if self.visible_items.len() == 0 {
self.selected_index = 0;
return;
} }
while matches!(
self.visible_items.get(index),
None | Some(ListItemType::BucketSeparator(..))
) {
index = match bias {
Bias::Left => {
if index == 0 {
self.visible_items.len() - 1
} else {
index - 1
}
}
Bias::Right => {
if index >= self.visible_items.len() - 1 {
0
} else {
index + 1
}
}
};
}
self.selected_index = index;
self.scroll_handle
.scroll_to_item(index, ScrollStrategy::Top);
cx.notify()
} }
pub fn select_previous( pub fn select_previous(
@ -293,13 +270,10 @@ impl AcpThreadHistory {
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let count = self.matched_count(); if self.selected_index == 0 {
if count > 0 { self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
if self.selected_index == 0 { } else {
self.set_selected_entry_index(count - 1, cx); self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
} else {
self.set_selected_entry_index(self.selected_index - 1, cx);
}
} }
} }
@ -309,13 +283,10 @@ impl AcpThreadHistory {
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let count = self.matched_count(); if self.selected_index == self.visible_items.len() - 1 {
if count > 0 { self.set_selected_index(0, Bias::Right, cx);
if self.selected_index == count - 1 { } else {
self.set_selected_entry_index(0, cx); self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
} else {
self.set_selected_entry_index(self.selected_index + 1, cx);
}
} }
} }
@ -325,35 +296,47 @@ impl AcpThreadHistory {
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let count = self.matched_count(); self.set_selected_index(0, Bias::Right, cx);
if count > 0 {
self.set_selected_entry_index(0, cx);
}
} }
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) { fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let count = self.matched_count(); self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
if count > 0 {
self.set_selected_entry_index(count - 1, cx);
}
} }
fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) { fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
self.selected_index = entry_index; self.confirm_entry(self.selected_index, cx);
}
let scroll_ix = match self.search_state { fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
SearchState::Empty | SearchState::Searching { .. } => self let Some(entry) = self.get_history_entry(ix) else {
.separated_item_indexes return;
.get(entry_index) };
.map(|ix| *ix as usize) cx.emit(ThreadHistoryEvent::Open(entry.clone()));
.unwrap_or(entry_index + 1), }
SearchState::Searched { .. } => entry_index,
fn remove_selected_thread(
&mut self,
_: &RemoveSelectedThread,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.remove_thread(self.selected_index, cx)
}
fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
let Some(entry) = self.get_history_entry(visible_item_ix) else {
return;
}; };
self.scroll_handle let task = match entry {
.scroll_to_item(scroll_ix, ScrollStrategy::Top); HistoryEntry::AcpThread(thread) => self
.history_store
cx.notify(); .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| {
this.delete_text_thread(context.path.clone(), cx)
}),
};
task.detach_and_log_err(cx);
} }
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> { fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
@ -393,91 +376,33 @@ impl AcpThreadHistory {
) )
} }
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) { fn render_list_items(
self.confirm_entry(self.selected_index, cx);
}
fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
let Some(entry) = self.get_match(ix) else {
return;
};
cx.emit(ThreadHistoryEvent::Open(entry.clone()));
}
fn remove_selected_thread(
&mut self,
_: &RemoveSelectedThread,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.remove_thread(self.selected_index, cx)
}
fn remove_thread(&mut self, ix: usize, cx: &mut Context<Self>) {
let Some(entry) = self.get_match(ix) else {
return;
};
let task = match entry {
HistoryEntry::AcpThread(thread) => self
.history_store
.update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| {
this.delete_text_thread(context.path.clone(), cx)
}),
};
task.detach_and_log_err(cx);
}
fn list_items(
&mut self, &mut self,
range: Range<usize>, range: Range<usize>,
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Vec<AnyElement> { ) -> Vec<AnyElement> {
match &self.search_state { self.visible_items
SearchState::Empty => self .get(range.clone())
.separated_items .into_iter()
.get(range) .flatten()
.iter() .enumerate()
.flat_map(|items| { .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
items .collect()
.iter()
.map(|item| self.render_list_item(item, vec![], cx))
})
.collect(),
SearchState::Searched { matches, .. } => matches[range]
.iter()
.filter_map(|m| {
let entry = self.all_entries.get(m.candidate_id)?;
Some(self.render_history_entry(
entry,
EntryTimeFormat::DateAndTime,
m.candidate_id,
m.positions.clone(),
cx,
))
})
.collect(),
SearchState::Searching { .. } => {
vec![]
}
}
} }
fn render_list_item( fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
&self,
item: &ListItemType,
highlight_positions: Vec<usize>,
cx: &Context<Self>,
) -> AnyElement {
match item { match item {
ListItemType::Entry { index, format } => match self.all_entries.get(*index) { ListItemType::Entry { entry, format } => self
Some(entry) => self .render_history_entry(entry, *format, ix, Vec::default(), cx)
.render_history_entry(entry, *format, *index, highlight_positions, cx) .into_any(),
.into_any(), ListItemType::SearchResult { entry, positions } => self.render_history_entry(
None => Empty.into_any_element(), entry,
}, EntryTimeFormat::DateAndTime,
ix,
positions.clone(),
cx,
),
ListItemType::BucketSeparator(bucket) => div() ListItemType::BucketSeparator(bucket) => div()
.px(DynamicSpacing::Base06.rems(cx)) .px(DynamicSpacing::Base06.rems(cx))
.pt_2() .pt_2()
@ -495,12 +420,12 @@ impl AcpThreadHistory {
&self, &self,
entry: &HistoryEntry, entry: &HistoryEntry,
format: EntryTimeFormat, format: EntryTimeFormat,
list_entry_ix: usize, ix: usize,
highlight_positions: Vec<usize>, highlight_positions: Vec<usize>,
cx: &Context<Self>, cx: &Context<Self>,
) -> AnyElement { ) -> AnyElement {
let selected = list_entry_ix == self.selected_index; let selected = ix == self.selected_index;
let hovered = Some(list_entry_ix) == self.hovered_index; let hovered = Some(ix) == self.hovered_index;
let timestamp = entry.updated_at().timestamp(); let timestamp = entry.updated_at().timestamp();
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone); let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
@ -508,7 +433,7 @@ impl AcpThreadHistory {
.w_full() .w_full()
.pb_1() .pb_1()
.child( .child(
ListItem::new(list_entry_ix) ListItem::new(ix)
.rounded() .rounded()
.toggle_state(selected) .toggle_state(selected)
.spacing(ListItemSpacing::Sparse) .spacing(ListItemSpacing::Sparse)
@ -530,8 +455,8 @@ impl AcpThreadHistory {
) )
.on_hover(cx.listener(move |this, is_hovered, _window, cx| { .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered { if *is_hovered {
this.hovered_index = Some(list_entry_ix); this.hovered_index = Some(ix);
} else if this.hovered_index == Some(list_entry_ix) { } else if this.hovered_index == Some(ix) {
this.hovered_index = None; this.hovered_index = None;
} }
@ -546,16 +471,14 @@ impl AcpThreadHistory {
.tooltip(move |window, cx| { .tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
}) })
.on_click(cx.listener(move |this, _, _, cx| { .on_click(
this.remove_thread(list_entry_ix, cx) cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)),
})), ),
) )
} else { } else {
None None
}) })
.on_click( .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))),
cx.listener(move |this, _, _, cx| this.confirm_entry(list_entry_ix, cx)),
),
) )
.into_any_element() .into_any_element()
} }
@ -578,7 +501,7 @@ impl Render for AcpThreadHistory {
.on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread)) .on_action(cx.listener(Self::remove_selected_thread))
.when(!self.all_entries.is_empty(), |parent| { .when(!self.history_store.read(cx).is_empty(cx), |parent| {
parent.child( parent.child(
h_flex() h_flex()
.h(px(41.)) // Match the toolbar perfectly .h(px(41.)) // Match the toolbar perfectly
@ -604,7 +527,7 @@ impl Render for AcpThreadHistory {
.overflow_hidden() .overflow_hidden()
.flex_grow(); .flex_grow();
if self.all_entries.is_empty() { if self.history_store.read(cx).is_empty(cx) {
view.justify_center() view.justify_center()
.child( .child(
h_flex().w_full().justify_center().child( h_flex().w_full().justify_center().child(
@ -623,9 +546,9 @@ impl Render for AcpThreadHistory {
.child( .child(
uniform_list( uniform_list(
"thread-history", "thread-history",
self.list_item_count(), self.visible_items.len(),
cx.processor(|this, range: Range<usize>, window, cx| { cx.processor(|this, range: Range<usize>, window, cx| {
this.list_items(range, window, cx) this.render_list_items(range, window, cx)
}), }),
) )
.p_1() .p_1()

View file

@ -2538,9 +2538,9 @@ impl AcpThreadView {
) )
}) })
.when(render_history, |this| { .when(render_history, |this| {
let recent_history = self let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| {
.history_store history_store.entries().take(3).collect()
.update(cx, |history_store, cx| history_store.recent_entries(3, cx)); });
this.justify_end().child( this.justify_end().child(
v_flex() v_flex()
.child( .child(